@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.
Files changed (41) hide show
  1. package/README.md +3 -13
  2. package/package.json +1 -1
  3. package/src/api/server.ts +4 -6
  4. package/src/cache/memory-cache.ts +5 -3
  5. package/src/core/account-manager.ts +1 -1
  6. package/src/core/accounts.ts +1 -1
  7. package/src/login.ts +2 -2
  8. package/src/routes/chat.ts +122 -91
  9. package/src/routes/upload.ts +5 -5
  10. package/src/services/playwright.ts +40 -120
  11. package/src/services/qwen.ts +29 -27
  12. package/src/tests/concurrency.test.ts +1 -1
  13. package/src/tests/concurrentChat.test.ts +1 -1
  14. package/src/tests/contextTruncation.test.ts +142 -0
  15. package/src/tests/delta.test.ts +80 -10
  16. package/src/tests/jsonFix.test.ts +110 -98
  17. package/src/tests/multimodal.test.ts +1 -1
  18. package/src/tests/parser.test.ts +40 -2
  19. package/src/tools/parser.ts +98 -33
  20. package/src/utils/context-truncation.ts +1 -6
  21. package/src/utils/json.ts +9 -8
  22. package/src/utils/types.ts +1 -1
  23. package/src/linter/extraction-engine.ts +0 -165
  24. package/src/linter/index.ts +0 -258
  25. package/src/linter/repair-normalize.ts +0 -245
  26. package/src/linter/safety-gate.ts +0 -219
  27. package/src/linter/streaming-state-machine.ts +0 -252
  28. package/src/linter/structural-parser.ts +0 -352
  29. package/src/linter/types.ts +0 -74
  30. package/src/tests/linter.test.ts +0 -151
  31. package/src/tests/parallel.test.ts +0 -42
  32. package/src/tests/structureVerification.test.ts +0 -176
  33. package/src/tools/ast.ts +0 -15
  34. package/src/tools/coercion.ts +0 -67
  35. package/src/tools/confidence.ts +0 -48
  36. package/src/tools/detector.ts +0 -40
  37. package/src/tools/executor.ts +0 -236
  38. package/src/tools/pipeline.ts +0 -122
  39. package/src/tools/registry-runtime.ts +0 -34
  40. package/src/tools/repair.ts +0 -42
  41. 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 Executor]
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
- │ │ └── chat.ts # Handler /v1/chat/completions
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pedrofariasx/qwenproxy",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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/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.ts')
73
+ const { loadAccounts } = await import('../core/accounts.js')
74
74
  const accounts = loadAccounts()
75
75
 
76
- const { initPlaywright, initPlaywrightForAccount, getQwenHeaders } = await import('../services/playwright.ts')
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.ts')
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 { cleanupAllAccountMutexes } = await import('../routes/chat.js')
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
- return Buffer.byteLength(key) + Buffer.byteLength(JSON.stringify(value))
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', Buffer.byteLength(serialized))
89
+ metrics.histogram('cache.value.size', entrySize)
88
90
  }
89
91
 
90
92
  async get<T>(key: CacheKey): Promise<T | null> {
@@ -1,4 +1,4 @@
1
- import { QwenAccount, loadAccounts } from './accounts.ts'
1
+ import { QwenAccount, loadAccounts } from './accounts.js'
2
2
  import { config } from './config.js'
3
3
 
4
4
  let currentIndex = 0
@@ -1,5 +1,5 @@
1
1
  import crypto from 'crypto'
2
- import { getDatabase } from './database.ts'
2
+ import { getDatabase } from './database.js'
3
3
  import { config } from './config.js'
4
4
 
5
5
  export interface QwenAccount {
package/src/login.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { addAccount, removeAccount, listAccounts, getAccountCredentials, QwenAccount } from './core/accounts.ts'
2
- import { initPlaywrightForAccount, closePlaywrightForAccount, BrowserType, launchManualLoginAccount, extractAccountInfoFromContext } from './services/playwright.ts'
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
 
@@ -10,48 +10,85 @@
10
10
 
11
11
  import { Context } from 'hono';
12
12
  import { stream as honoStream } from 'hono/streaming';
13
- import { v4 as uuidv4 } from 'uuid';
14
- import { createQwenStream, updateSessionParent } from '../services/qwen.ts';
15
- import { OpenAIRequest, ChoiceDelta, Message } from '../utils/types.ts';
16
- import { registry } from '../tools/registry.ts';
17
- import type { FunctionToolDefinition } from '../tools/types.ts';
18
- import { robustParseJSON } from '../utils/json.ts';
19
- import { StreamingToolParser } from '../tools/parser.ts';
20
- import { QwenStreamParser, ParsedChunkResult } from '../utils/qwen-stream-parser.ts';
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.ts';
24
- import { getNextAccount, getNextAvailableAccount, markAccountRateLimited, getAccountCooldownInfo } from '../core/account-manager.ts';
25
- import { registerStream, removeStream, getStream } from '../core/stream-registry.ts';
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 { delta: newStr, matchedContent: newStr };
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
- // Fast path: incremental SSE streams append to oldStr most of the time
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 { delta: newStr, matchedContent: oldStr + newStr };
76
+ return {
77
+ delta: newStr,
78
+ matchedContent: oldStr + newStr,
79
+ contentLength: newStr.length,
80
+ contentSuffix: newStr.slice(-64)
81
+ };
50
82
  }
51
- return { delta, matchedContent: newStr };
83
+ return {
84
+ delta,
85
+ matchedContent: newStr,
86
+ contentLength: newStr.length,
87
+ contentSuffix: newStr.slice(-64)
88
+ };
52
89
  }
53
90
 
54
- // Fallback: segment-based prefix matching
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 { delta: newStr.substring(commonPrefixLen), matchedContent: newStr };
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
- return { delta: newStr, matchedContent: oldStr + newStr };
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
- // Handle multimodal content (text + images + videos + audio + files)
123
- const multimodalParts = msg.content.filter(
124
- (p: any) =>
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
- let triedAccountIds = new Set<string>();
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-' + uuidv4();
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
- // Send initial chunk
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
- let startIdx = 0;
515
- let newlineIdx: number;
516
- while ((newlineIdx = buffer.indexOf('\n', startIdx)) !== -1) {
517
- const line = buffer.slice(startIdx, newlineIdx);
518
- startIdx = newlineIdx + 1;
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
- const trimmed = line.trim();
521
- if (!trimmed || !trimmed.startsWith('data: ')) continue;
572
+ const trimmed = line.trim();
573
+ if (!trimmed || !trimmed.startsWith('data: ')) continue;
522
574
 
523
- const dataStr = trimmed.slice(6);
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
- streamWriter.write(`data: ${JSON.stringify({
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
- streamWriter.write(`data: ${JSON.stringify({
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
- streamWriter.write(`data: ${JSON.stringify({
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
- // Trim processed portion from buffer
644
- if (startIdx > 0) {
645
- buffer = buffer.slice(startIdx);
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': uuidv4(),
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'],
@@ -5,8 +5,8 @@
5
5
  */
6
6
 
7
7
  import { Context } from "hono";
8
- import { getQwenHeaders } from "../services/playwright.ts";
9
- import { v4 as uuidv4 } from "uuid";
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": uuidv4(),
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: uuidv4(),
726
+ itemId: crypto.randomUUID(),
727
727
  file_type: typeInfo.mime,
728
728
  showType: typeInfo.showType,
729
729
  file_class: typeInfo.fileClass,
730
- uploadTaskId: uuidv4(),
730
+ uploadTaskId: crypto.randomUUID(),
731
731
  });
732
732
  }
733
733
  }