@pedrofariasx/qwenproxy 1.2.1 → 1.2.3
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/README.md +3 -13
- package/package.json +1 -1
- package/src/api/server.ts +4 -6
- package/src/cache/memory-cache.ts +5 -3
- package/src/core/account-manager.ts +1 -1
- package/src/core/accounts.ts +1 -1
- package/src/login.ts +2 -2
- package/src/routes/chat.ts +122 -91
- package/src/routes/upload.ts +5 -5
- package/src/services/playwright.ts +40 -120
- package/src/services/qwen.ts +29 -27
- package/src/tests/concurrency.test.ts +1 -1
- package/src/tests/concurrentChat.test.ts +1 -1
- package/src/tests/contextTruncation.test.ts +142 -0
- package/src/tests/delta.test.ts +80 -10
- package/src/tests/jsonFix.test.ts +110 -98
- package/src/tests/multimodal.test.ts +1 -1
- package/src/tests/parser.test.ts +40 -2
- package/src/tools/parser.ts +98 -33
- package/src/utils/context-truncation.ts +1 -6
- package/src/utils/json.ts +9 -8
- package/src/utils/types.ts +1 -1
- package/src/linter/extraction-engine.ts +0 -165
- package/src/linter/index.ts +0 -258
- package/src/linter/repair-normalize.ts +0 -245
- package/src/linter/safety-gate.ts +0 -219
- package/src/linter/streaming-state-machine.ts +0 -252
- package/src/linter/structural-parser.ts +0 -352
- package/src/linter/types.ts +0 -74
- package/src/tests/linter.test.ts +0 -151
- package/src/tests/parallel.test.ts +0 -42
- package/src/tests/structureVerification.test.ts +0 -176
- package/src/tools/ast.ts +0 -15
- package/src/tools/coercion.ts +0 -67
- package/src/tools/confidence.ts +0 -48
- package/src/tools/detector.ts +0 -40
- package/src/tools/executor.ts +0 -236
- package/src/tools/pipeline.ts +0 -122
- package/src/tools/registry-runtime.ts +0 -34
- package/src/tools/repair.ts +0 -42
- package/src/tools/validator.ts +0 -33
package/README.md
CHANGED
|
@@ -39,7 +39,7 @@ graph TD
|
|
|
39
39
|
Playwright --> Browser2[Browser - Conta 2]
|
|
40
40
|
Playwright --> BrowserN[Browser - Conta N]
|
|
41
41
|
Handler --> QwenAPI[chat.qwen.ai]
|
|
42
|
-
Handler --> Tools[Tool
|
|
42
|
+
Handler --> Tools[Tool Parser]
|
|
43
43
|
|
|
44
44
|
subgraph "Persistência"
|
|
45
45
|
Accounts
|
|
@@ -233,24 +233,14 @@ qwenproxy/
|
|
|
233
233
|
│ │ ├── model-registry.ts # Registro de modelos e context windows
|
|
234
234
|
│ │ ├── stream-registry.ts # Tracking de streams ativos
|
|
235
235
|
│ │ └── watchdog.ts # Health monitoring
|
|
236
|
-
│ ├── linter/
|
|
237
|
-
│ │ ├── bar.ts # Facade
|
|
238
|
-
│ │ ├── extraction-engine.ts # Extraction engine
|
|
239
|
-
│ │ ├── foo.ts # Exports
|
|
240
|
-
│ │ ├── index.ts # Main public API
|
|
241
|
-
│ │ ├── repair-normalize.ts # Repair and normalize
|
|
242
|
-
│ │ ├── safety-gate.ts # Safety gate
|
|
243
|
-
│ │ ├── streaming-state-machine.ts # Streaming state machine
|
|
244
|
-
│ │ ├── structural-parser.ts # Structural parser
|
|
245
|
-
│ │ └── types.ts # Types
|
|
246
236
|
│ ├── routes/
|
|
247
|
-
│ │
|
|
237
|
+
│ │ ├── chat.ts # Handler /v1/chat/completions
|
|
238
|
+
│ │ └── upload.ts # Handler /v1/upload (multimodal)
|
|
248
239
|
│ ├── services/
|
|
249
240
|
│ │ ├── playwright.ts # Automação de navegador
|
|
250
241
|
│ │ └── qwen.ts # Integração com API do Qwen
|
|
251
242
|
│ ├── tests/ # Testes automatizados
|
|
252
243
|
│ ├── tools/
|
|
253
|
-
│ │ ├── executor.ts # Execução de ferramentas
|
|
254
244
|
│ │ ├── parser.ts # Parser de <tool_call> tags
|
|
255
245
|
│ │ ├── registry.ts # Registro de tools
|
|
256
246
|
│ │ ├── schema.ts # Validação JSON Schema
|
package/package.json
CHANGED
package/src/api/server.ts
CHANGED
|
@@ -70,10 +70,10 @@ app.notFound((c) => c.json({ error: 'Not found' }, 404))
|
|
|
70
70
|
export async function startServer(): Promise<void> {
|
|
71
71
|
await cache.connect()
|
|
72
72
|
|
|
73
|
-
const { loadAccounts } = await import('../core/accounts.
|
|
73
|
+
const { loadAccounts } = await import('../core/accounts.js')
|
|
74
74
|
const accounts = loadAccounts()
|
|
75
75
|
|
|
76
|
-
const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.
|
|
76
|
+
const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.js')
|
|
77
77
|
|
|
78
78
|
await initPlaywright(config.browser.headless)
|
|
79
79
|
|
|
@@ -87,7 +87,7 @@ export async function startServer(): Promise<void> {
|
|
|
87
87
|
)
|
|
88
88
|
)
|
|
89
89
|
console.log('[Server] Pre-fetching headers for all accounts in background...')
|
|
90
|
-
const { warmAllPools } = await import('../services/qwen.
|
|
90
|
+
const { warmAllPools } = await import('../services/qwen.js')
|
|
91
91
|
warmAllPools(accounts.map(a => a.id)).catch(() => {})
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -111,9 +111,7 @@ export async function startServer(): Promise<void> {
|
|
|
111
111
|
await cache.close()
|
|
112
112
|
const { closePlaywright } = await import('../services/playwright.js')
|
|
113
113
|
await closePlaywright()
|
|
114
|
-
const {
|
|
115
|
-
cleanupAllAccountMutexes()
|
|
116
|
-
const { closeDatabase } = await import('../core/database.ts')
|
|
114
|
+
const { closeDatabase } = await import('../core/database.js')
|
|
117
115
|
closeDatabase()
|
|
118
116
|
server?.close()
|
|
119
117
|
process.exit(0)
|
|
@@ -21,6 +21,7 @@ export class MemoryCache {
|
|
|
21
21
|
private cleanupInterval: NodeJS.Timeout | null
|
|
22
22
|
private maxEntries: number
|
|
23
23
|
private totalBytes: number
|
|
24
|
+
private scanRegexCache: Map<string, RegExp>
|
|
24
25
|
|
|
25
26
|
constructor(options?: { prefix?: string; defaultTTL?: number; maxEntries?: number }) {
|
|
26
27
|
this.prefix = options?.prefix || 'qwenproxy:'
|
|
@@ -29,12 +30,14 @@ export class MemoryCache {
|
|
|
29
30
|
this.store = new Map()
|
|
30
31
|
this.totalBytes = 0
|
|
31
32
|
this.cleanupInterval = null
|
|
33
|
+
this.scanRegexCache = new Map()
|
|
32
34
|
|
|
33
35
|
this.startCleanup()
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
private entryByteSize(key: string, value: any): number {
|
|
37
|
-
|
|
39
|
+
const valueStr = typeof value === 'string' ? value : JSON.stringify(value)
|
|
40
|
+
return Buffer.byteLength(key) + Buffer.byteLength(valueStr || '')
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
private evictLRU(): void {
|
|
@@ -63,7 +66,6 @@ export class MemoryCache {
|
|
|
63
66
|
}
|
|
64
67
|
|
|
65
68
|
async set<T>(key: CacheKey, value: T, ttl?: number): Promise<void> {
|
|
66
|
-
const serialized = JSON.stringify(value)
|
|
67
69
|
const effectiveTTL = ttl || this.defaultTTL
|
|
68
70
|
const fullKey = this.prefix + key
|
|
69
71
|
const entrySize = this.entryByteSize(fullKey, value)
|
|
@@ -84,7 +86,7 @@ export class MemoryCache {
|
|
|
84
86
|
this.totalBytes += entrySize
|
|
85
87
|
|
|
86
88
|
metrics.increment('cache.set')
|
|
87
|
-
metrics.histogram('cache.value.size',
|
|
89
|
+
metrics.histogram('cache.value.size', entrySize)
|
|
88
90
|
}
|
|
89
91
|
|
|
90
92
|
async get<T>(key: CacheKey): Promise<T | null> {
|
package/src/core/accounts.ts
CHANGED
package/src/login.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.
|
|
2
|
-
import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.
|
|
1
|
+
import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.js'
|
|
2
|
+
import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.js'
|
|
3
3
|
import * as readline from 'readline'
|
|
4
4
|
import * as dotenv from 'dotenv'
|
|
5
5
|
|
package/src/routes/chat.ts
CHANGED
|
@@ -10,48 +10,85 @@
|
|
|
10
10
|
|
|
11
11
|
import { Context } from 'hono';
|
|
12
12
|
import { stream as honoStream } from 'hono/streaming';
|
|
13
|
-
import
|
|
14
|
-
import { createQwenStream, updateSessionParent } from '../services/qwen.
|
|
15
|
-
import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.
|
|
16
|
-
import { registry } from '../tools/registry.
|
|
17
|
-
import type { FunctionToolDefinition } from '../tools/types.
|
|
18
|
-
import { robustParseJSON } from '../utils/json.
|
|
19
|
-
import { StreamingToolParser } from '../tools/parser.
|
|
20
|
-
import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.
|
|
21
|
-
import { RetryableQwenStreamError } from '../services/qwen.ts';
|
|
13
|
+
import crypto from 'crypto';
|
|
14
|
+
import { createQwenStream, updateSessionParent, RetryableQwenStreamError } from '../services/qwen.js';
|
|
15
|
+
import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.js';
|
|
16
|
+
import { registry } from '../tools/registry.js';
|
|
17
|
+
import type { FunctionToolDefinition } from '../tools/types.js';
|
|
18
|
+
import { robustParseJSON } from '../utils/json.js';
|
|
19
|
+
import { StreamingToolParser } from '../tools/parser.js';
|
|
20
|
+
import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.js';
|
|
22
21
|
import { getModelContextWindow } from '../core/model-registry.js'
|
|
23
|
-
import { truncateMessages, estimateTokenCount } from '../utils/context-truncation.
|
|
24
|
-
import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.
|
|
25
|
-
import { registerStream, removeStream, getStream } from '../core/stream-registry.
|
|
22
|
+
import { truncateMessages, estimateTokenCount } from '../utils/context-truncation.js';
|
|
23
|
+
import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.js';
|
|
24
|
+
import { registerStream, removeStream, getStream } from '../core/stream-registry.js';
|
|
26
25
|
import { metrics } from '../core/metrics.js'
|
|
27
26
|
|
|
28
|
-
export function cleanupAllAccountMutexes(): void {
|
|
29
|
-
// No-op - kept for backward compatibility
|
|
30
|
-
}
|
|
31
|
-
|
|
32
27
|
export interface DeltaResult {
|
|
33
28
|
delta: string;
|
|
34
29
|
matchedContent: string;
|
|
30
|
+
contentLength: number;
|
|
31
|
+
contentSuffix: string;
|
|
35
32
|
}
|
|
36
33
|
|
|
37
|
-
export function getIncrementalDelta(oldStr: string, newStr: string): DeltaResult {
|
|
34
|
+
export function getIncrementalDelta(oldStr: string, newStr: string, prevLength: number = 0, prevSuffix: string = ''): DeltaResult {
|
|
38
35
|
if (!oldStr) {
|
|
39
|
-
return {
|
|
36
|
+
return {
|
|
37
|
+
delta: newStr,
|
|
38
|
+
matchedContent: newStr,
|
|
39
|
+
contentLength: newStr.length,
|
|
40
|
+
contentSuffix: newStr.slice(-64)
|
|
41
|
+
};
|
|
40
42
|
}
|
|
41
43
|
if (newStr === oldStr) {
|
|
42
|
-
return { delta: '', matchedContent: oldStr };
|
|
44
|
+
return { delta: '', matchedContent: oldStr, contentLength: prevLength, contentSuffix: prevSuffix };
|
|
43
45
|
}
|
|
44
46
|
|
|
45
|
-
//
|
|
47
|
+
// Ultra-fast path: use length tracking to avoid O(n) startsWith on large strings
|
|
48
|
+
if (newStr.length > prevLength && prevLength > 0) {
|
|
49
|
+
const delta = newStr.slice(prevLength);
|
|
50
|
+
const checkLen = Math.min(64, prevLength);
|
|
51
|
+
const expectedSuffix = prevSuffix.slice(-checkLen);
|
|
52
|
+
const actualSuffix = newStr.slice(prevLength - checkLen, prevLength);
|
|
53
|
+
|
|
54
|
+
if (expectedSuffix === actualSuffix) {
|
|
55
|
+
if (delta.length <= 4 && oldStr.length > 2000) {
|
|
56
|
+
return {
|
|
57
|
+
delta: newStr,
|
|
58
|
+
matchedContent: oldStr + newStr,
|
|
59
|
+
contentLength: newStr.length,
|
|
60
|
+
contentSuffix: newStr.slice(-64)
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
delta,
|
|
65
|
+
matchedContent: newStr,
|
|
66
|
+
contentLength: newStr.length,
|
|
67
|
+
contentSuffix: newStr.slice(-64)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Fallback: startsWith check for edge cases
|
|
46
73
|
if (newStr.startsWith(oldStr)) {
|
|
47
74
|
const delta = newStr.slice(oldStr.length);
|
|
48
75
|
if (delta.length <= 4 && oldStr.length > 2000) {
|
|
49
|
-
return {
|
|
76
|
+
return {
|
|
77
|
+
delta: newStr,
|
|
78
|
+
matchedContent: oldStr + newStr,
|
|
79
|
+
contentLength: newStr.length,
|
|
80
|
+
contentSuffix: newStr.slice(-64)
|
|
81
|
+
};
|
|
50
82
|
}
|
|
51
|
-
return {
|
|
83
|
+
return {
|
|
84
|
+
delta,
|
|
85
|
+
matchedContent: newStr,
|
|
86
|
+
contentLength: newStr.length,
|
|
87
|
+
contentSuffix: newStr.slice(-64)
|
|
88
|
+
};
|
|
52
89
|
}
|
|
53
90
|
|
|
54
|
-
//
|
|
91
|
+
// Segment-based prefix matching (rare path)
|
|
55
92
|
const scanWindow = Math.min(2000, oldStr.length);
|
|
56
93
|
const maxLen = Math.min(scanWindow, newStr.length);
|
|
57
94
|
|
|
@@ -65,17 +102,27 @@ export function getIncrementalDelta(oldStr: string, newStr: string): DeltaResult
|
|
|
65
102
|
commonPrefixLen += segmentLen;
|
|
66
103
|
}
|
|
67
104
|
|
|
68
|
-
// Fine-grained scan within the mismatching segment
|
|
69
105
|
while (commonPrefixLen < maxLen && oldStr[commonPrefixLen] === newStr[commonPrefixLen]) {
|
|
70
106
|
commonPrefixLen++;
|
|
71
107
|
}
|
|
72
108
|
|
|
73
109
|
const threshold = Math.min(scanWindow, 4);
|
|
74
110
|
if (commonPrefixLen >= threshold) {
|
|
75
|
-
return {
|
|
111
|
+
return {
|
|
112
|
+
delta: newStr.substring(commonPrefixLen),
|
|
113
|
+
matchedContent: newStr,
|
|
114
|
+
contentLength: newStr.length,
|
|
115
|
+
contentSuffix: newStr.slice(-64)
|
|
116
|
+
};
|
|
76
117
|
}
|
|
77
118
|
|
|
78
|
-
|
|
119
|
+
const combined = oldStr + newStr;
|
|
120
|
+
return {
|
|
121
|
+
delta: newStr,
|
|
122
|
+
matchedContent: combined,
|
|
123
|
+
contentLength: combined.length,
|
|
124
|
+
contentSuffix: combined.slice(-64)
|
|
125
|
+
};
|
|
79
126
|
}
|
|
80
127
|
|
|
81
128
|
function parseQwenErrorPayload(raw: string): { message: string; status: number } | null {
|
|
@@ -119,29 +166,26 @@ export async function chatCompletions(c: Context) {
|
|
|
119
166
|
const msg = messages[i];
|
|
120
167
|
let contentStr = '';
|
|
121
168
|
if (Array.isArray(msg.content)) {
|
|
122
|
-
//
|
|
123
|
-
const
|
|
124
|
-
|
|
169
|
+
// Single-pass: extract text and multimodal parts in one iteration
|
|
170
|
+
const textParts: string[] = [];
|
|
171
|
+
const multimodalParts: Array<{ type: string; text?: string; image_url?: { url: string }; video_url?: { url: string }; audio_url?: { url: string }; file_url?: { url: string } }> = [];
|
|
172
|
+
|
|
173
|
+
for (const p of msg.content as any[]) {
|
|
174
|
+
if (p.type === "text" && p.text) {
|
|
175
|
+
textParts.push(p.text);
|
|
176
|
+
} else if (
|
|
125
177
|
(p.type === "image_url" && p.image_url?.url) ||
|
|
126
178
|
(p.type === "video_url" && p.video_url?.url) ||
|
|
127
179
|
(p.type === "audio_url" && p.audio_url?.url) ||
|
|
128
|
-
(p.type === "file_url" && p.file_url?.url)
|
|
129
|
-
|
|
130
|
-
|
|
180
|
+
(p.type === "file_url" && p.file_url?.url)
|
|
181
|
+
) {
|
|
182
|
+
multimodalParts.push(p);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
contentStr = textParts.join("\n");
|
|
131
187
|
if (multimodalParts.length > 0) {
|
|
132
|
-
// Defer processing to after account selection to reuse cached headers
|
|
133
188
|
pendingMultimodal.push(multimodalParts);
|
|
134
|
-
// Extract text parts for prompt building
|
|
135
|
-
contentStr = msg.content
|
|
136
|
-
.filter((p: any) => p.type === "text")
|
|
137
|
-
.map((p: any) => p.text)
|
|
138
|
-
.join("\n");
|
|
139
|
-
} else {
|
|
140
|
-
// No multimodal parts, just extract text
|
|
141
|
-
contentStr = msg.content
|
|
142
|
-
.filter((p: any) => p.type === "text")
|
|
143
|
-
.map((p: any) => p.text)
|
|
144
|
-
.join("\n");
|
|
145
189
|
}
|
|
146
190
|
} else if (typeof msg.content === 'object' && msg.content !== null) {
|
|
147
191
|
contentStr = JSON.stringify(msg.content);
|
|
@@ -244,12 +288,12 @@ export async function chatCompletions(c: Context) {
|
|
|
244
288
|
|
|
245
289
|
// Account selection with fallback on rate-limit/failure
|
|
246
290
|
let account = getNextAccount();
|
|
247
|
-
|
|
291
|
+
const triedAccountIds = new Set<string>();
|
|
248
292
|
let lastError: any = null;
|
|
249
293
|
|
|
250
294
|
let stream: ReadableStream | undefined;
|
|
251
295
|
let uiSessionId = '';
|
|
252
|
-
const completionId = 'chatcmpl-' +
|
|
296
|
+
const completionId = 'chatcmpl-' + crypto.randomUUID();
|
|
253
297
|
|
|
254
298
|
while (account) {
|
|
255
299
|
const accountId = account.id;
|
|
@@ -350,7 +394,6 @@ export async function chatCompletions(c: Context) {
|
|
|
350
394
|
|
|
351
395
|
const toolCallsOut: any[] = [];
|
|
352
396
|
let buffer = '';
|
|
353
|
-
const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
|
|
354
397
|
|
|
355
398
|
const qwenParser = new QwenStreamParser(uiSessionId, {
|
|
356
399
|
tools: hasTools ? bodyAny.tools : [],
|
|
@@ -476,10 +519,18 @@ export async function chatCompletions(c: Context) {
|
|
|
476
519
|
finish_reason: finishReason
|
|
477
520
|
});
|
|
478
521
|
|
|
479
|
-
// Pre-compute timestamp once before the stream loop
|
|
480
522
|
const createdTimestamp = Math.floor(Date.now() / 1000);
|
|
481
523
|
|
|
482
|
-
|
|
524
|
+
const fastWriteContent = (content: string) => {
|
|
525
|
+
const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
526
|
+
streamWriter.write(`data: {"id":"${completionId}","object":"chat.completion.chunk","created":${createdTimestamp},"model":"${body.model}","choices":[{"index":0,"delta":{"content":"${escaped}"},"logprobs":null,"finish_reason":null}]}\n\n`);
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const fastWriteReasoning = (content: string) => {
|
|
530
|
+
const escaped = content.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t');
|
|
531
|
+
streamWriter.write(`data: {"id":"${completionId}","object":"chat.completion.chunk","created":${createdTimestamp},"model":"${body.model}","choices":[{"index":0,"delta":{"reasoning_content":"${escaped}"},"logprobs":null,"finish_reason":null}]}\n\n`);
|
|
532
|
+
};
|
|
533
|
+
|
|
483
534
|
writeEvent({
|
|
484
535
|
id: completionId,
|
|
485
536
|
object: 'chat.completion.chunk',
|
|
@@ -493,34 +544,35 @@ export async function chatCompletions(c: Context) {
|
|
|
493
544
|
|
|
494
545
|
let reasoningBuffer = '';
|
|
495
546
|
let lastFullContent = '';
|
|
547
|
+
let contentLength = 0;
|
|
548
|
+
let contentSuffix = '';
|
|
496
549
|
let targetResponseId: string | null = null;
|
|
497
550
|
let targetResponseIdSet = false;
|
|
498
551
|
let currentThoughtIndex = 0;
|
|
499
|
-
const hasTools = Array.isArray(bodyAny.tools) && bodyAny.tools.length > 0;
|
|
500
552
|
const toolParser = hasTools ? new StreamingToolParser(bodyAny.tools) : null;
|
|
501
553
|
|
|
502
554
|
let buffer = '';
|
|
555
|
+
let bufferOffset = 0;
|
|
503
556
|
let completionTokens = 0;
|
|
504
557
|
let promptTokens = Math.ceil(finalPrompt.length / 3.5);
|
|
505
558
|
|
|
506
|
-
// Real-time flush: send each event immediately to minimize latency
|
|
507
|
-
let chunkCount = 0;
|
|
508
559
|
while (true) {
|
|
509
560
|
const { done, value } = await reader.read();
|
|
510
561
|
if (done) break;
|
|
511
562
|
|
|
512
563
|
buffer += decoder.decode(value, { stream: true });
|
|
513
564
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
565
|
+
while (bufferOffset < buffer.length) {
|
|
566
|
+
const newlineIdx = buffer.indexOf('\n', bufferOffset);
|
|
567
|
+
if (newlineIdx === -1) break;
|
|
568
|
+
|
|
569
|
+
const line = buffer.slice(bufferOffset, newlineIdx);
|
|
570
|
+
bufferOffset = newlineIdx + 1;
|
|
519
571
|
|
|
520
|
-
|
|
521
|
-
|
|
572
|
+
const trimmed = line.trim();
|
|
573
|
+
if (!trimmed || !trimmed.startsWith('data: ')) continue;
|
|
522
574
|
|
|
523
|
-
|
|
575
|
+
const dataStr = trimmed.slice(6);
|
|
524
576
|
if (dataStr === '[DONE]') {
|
|
525
577
|
streamWriter.write('data: [DONE]\n\n');
|
|
526
578
|
continue;
|
|
@@ -569,10 +621,12 @@ export async function chatCompletions(c: Context) {
|
|
|
569
621
|
isThinkingChunk = false;
|
|
570
622
|
if (delta.content !== undefined) {
|
|
571
623
|
const newContent = delta.content || '';
|
|
572
|
-
const result = getIncrementalDelta(lastFullContent, newContent);
|
|
624
|
+
const result = getIncrementalDelta(lastFullContent, newContent, contentLength, contentSuffix);
|
|
573
625
|
vStr = result.delta;
|
|
574
626
|
if (vStr) {
|
|
575
627
|
lastFullContent = result.matchedContent;
|
|
628
|
+
contentLength = result.contentLength;
|
|
629
|
+
contentSuffix = result.contentSuffix;
|
|
576
630
|
foundStr = true;
|
|
577
631
|
}
|
|
578
632
|
}
|
|
@@ -584,24 +638,12 @@ export async function chatCompletions(c: Context) {
|
|
|
584
638
|
|
|
585
639
|
if (isThinkingChunk) {
|
|
586
640
|
reasoningBuffer += vStr;
|
|
587
|
-
|
|
588
|
-
id: completionId,
|
|
589
|
-
object: 'chat.completion.chunk',
|
|
590
|
-
created: createdTimestamp,
|
|
591
|
-
model: body.model,
|
|
592
|
-
choices: [makeChoice({ reasoning_content: vStr })]
|
|
593
|
-
})}\n\n`);
|
|
641
|
+
fastWriteReasoning(vStr);
|
|
594
642
|
} else {
|
|
595
643
|
if (hasTools && toolParser) {
|
|
596
644
|
const { text, toolCalls } = toolParser.feed(vStr);
|
|
597
645
|
if (text) {
|
|
598
|
-
|
|
599
|
-
id: completionId,
|
|
600
|
-
object: 'chat.completion.chunk',
|
|
601
|
-
created: createdTimestamp,
|
|
602
|
-
model: body.model,
|
|
603
|
-
choices: [makeChoice({ content: text })]
|
|
604
|
-
})}\n\n`);
|
|
646
|
+
fastWriteContent(text);
|
|
605
647
|
}
|
|
606
648
|
for (const tc of toolCalls) {
|
|
607
649
|
streamWriter.write(`data: ${JSON.stringify({
|
|
@@ -624,13 +666,7 @@ export async function chatCompletions(c: Context) {
|
|
|
624
666
|
}
|
|
625
667
|
} else {
|
|
626
668
|
if (vStr) {
|
|
627
|
-
|
|
628
|
-
id: completionId,
|
|
629
|
-
object: 'chat.completion.chunk',
|
|
630
|
-
created: createdTimestamp,
|
|
631
|
-
model: body.model,
|
|
632
|
-
choices: [makeChoice({ content: vStr })]
|
|
633
|
-
})}\n\n`);
|
|
669
|
+
fastWriteContent(vStr);
|
|
634
670
|
}
|
|
635
671
|
}
|
|
636
672
|
}
|
|
@@ -640,16 +676,11 @@ export async function chatCompletions(c: Context) {
|
|
|
640
676
|
}
|
|
641
677
|
}
|
|
642
678
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
679
|
+
if (bufferOffset > 0) {
|
|
680
|
+
buffer = buffer.slice(bufferOffset);
|
|
681
|
+
bufferOffset = 0;
|
|
646
682
|
}
|
|
647
683
|
|
|
648
|
-
// Periodic yielding to prevent event loop starvation
|
|
649
|
-
chunkCount++;
|
|
650
|
-
if (chunkCount % 100 === 0) {
|
|
651
|
-
await new Promise(r => setTimeout(r, 0));
|
|
652
|
-
}
|
|
653
684
|
}
|
|
654
685
|
|
|
655
686
|
const upstreamError = parseQwenErrorPayload(buffer);
|
|
@@ -782,7 +813,7 @@ export async function chatCompletionsStop(c: Context) {
|
|
|
782
813
|
'Sec-Fetch-Mode': 'cors',
|
|
783
814
|
'Sec-Fetch-Site': 'same-origin',
|
|
784
815
|
'User-Agent': stream.headers['user-agent'],
|
|
785
|
-
'X-Request-Id':
|
|
816
|
+
'X-Request-Id': crypto.randomUUID(),
|
|
786
817
|
'bx-ua': stream.headers['bx-ua'],
|
|
787
818
|
'bx-umidtoken': stream.headers['bx-umidtoken'],
|
|
788
819
|
'bx-v': stream.headers['bx-v'],
|
package/src/routes/upload.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { Context } from "hono";
|
|
8
|
-
import { getQwenHeaders } from "../services/playwright.
|
|
9
|
-
import
|
|
8
|
+
import { getQwenHeaders } from "../services/playwright.js";
|
|
9
|
+
import crypto from "crypto";
|
|
10
10
|
|
|
11
11
|
interface STSResponse {
|
|
12
12
|
success: boolean;
|
|
@@ -46,7 +46,7 @@ async function getSTSToken(
|
|
|
46
46
|
Origin: "https://chat.qwen.ai",
|
|
47
47
|
Referer: "https://chat.qwen.ai/",
|
|
48
48
|
"User-Agent": headers["user-agent"],
|
|
49
|
-
"X-Request-Id":
|
|
49
|
+
"X-Request-Id": crypto.randomUUID(),
|
|
50
50
|
"bx-ua": headers["bx-ua"],
|
|
51
51
|
"bx-umidtoken": headers["bx-umidtoken"],
|
|
52
52
|
"bx-v": headers["bx-v"],
|
|
@@ -723,11 +723,11 @@ export async function processImagesForQwen(
|
|
|
723
723
|
greenNet: "success",
|
|
724
724
|
size: fileSize,
|
|
725
725
|
error: "",
|
|
726
|
-
itemId:
|
|
726
|
+
itemId: crypto.randomUUID(),
|
|
727
727
|
file_type: typeInfo.mime,
|
|
728
728
|
showType: typeInfo.showType,
|
|
729
729
|
file_class: typeInfo.fileClass,
|
|
730
|
-
uploadTaskId:
|
|
730
|
+
uploadTaskId: crypto.randomUUID(),
|
|
731
731
|
});
|
|
732
732
|
}
|
|
733
733
|
}
|