qwenproxy-cli 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/package.json +1 -1
- package/src/routes/anthropic/index.ts +17 -0
- package/src/routes/chat/context.ts +6 -5
- package/src/routes/chat/helpers.ts +23 -0
- package/src/routes/chat/streaming.ts +31 -10
- package/src/routes/chat/validation.ts +58 -8
- package/src/routes/responses/index.ts +19 -11
- package/src/services/playwright.ts +21 -1
- package/src/services/qwen.ts +11 -3
- package/src/tools/parser.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.3",
|
|
4
4
|
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"bin": {
|
|
@@ -97,6 +97,18 @@ app.post("/v1/messages", async (c) => {
|
|
|
97
97
|
try {
|
|
98
98
|
// 3. Translate Anthropic request to internal OpenAI format
|
|
99
99
|
const openaiRequest = translateAnthropicToOpenAI(body);
|
|
100
|
+
const claudeSessionId =
|
|
101
|
+
c.req.header("x-claude-code-session-id") ||
|
|
102
|
+
(typeof body.metadata?.user_id === "string" && body.metadata.user_id.includes('"session_id":')
|
|
103
|
+
? (() => {
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(body.metadata.user_id as string);
|
|
106
|
+
return typeof parsed?.session_id === "string" ? parsed.session_id : undefined;
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
})()
|
|
111
|
+
: undefined);
|
|
100
112
|
|
|
101
113
|
const dispatchToChat = (streamMode: boolean) =>
|
|
102
114
|
fetch(`http://127.0.0.1:${config.server.port}/v1/chat/completions`, {
|
|
@@ -105,9 +117,14 @@ app.post("/v1/messages", async (c) => {
|
|
|
105
117
|
"Content-Type": "application/json",
|
|
106
118
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
107
119
|
"x-qwenproxy-route": "Anthropic",
|
|
120
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
121
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
122
|
+
: {}),
|
|
123
|
+
...(claudeSessionId ? { "x-session-id": claudeSessionId } : {}),
|
|
108
124
|
},
|
|
109
125
|
body: JSON.stringify({
|
|
110
126
|
...openaiRequest,
|
|
127
|
+
...(claudeSessionId ? { session_id: claudeSessionId } : {}),
|
|
111
128
|
stream: streamMode,
|
|
112
129
|
...(streamMode ? { stream_options: { include_usage: true } } : {}),
|
|
113
130
|
}),
|
|
@@ -87,16 +87,17 @@ export async function buildFinalContext(
|
|
|
87
87
|
? false
|
|
88
88
|
: useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
|
|
89
89
|
|
|
90
|
-
// Compute sessionId:
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
90
|
+
// Compute sessionId: deterministic session ID derived from the conversation
|
|
91
|
+
// key (or "implicit-thread") + system instructions + first user message.
|
|
92
|
+
// Including completeInstructions in ALL modes (not just explicit keys)
|
|
93
|
+
// prevents cross-project session collisions when different projects share
|
|
94
|
+
// the same first user message but have different system prompts / tools.
|
|
94
95
|
const sessionId = isStateless
|
|
95
96
|
? null
|
|
96
97
|
: (conversationKey || useThreadNative)
|
|
97
98
|
? deriveSessionId(
|
|
98
99
|
messages,
|
|
99
|
-
|
|
100
|
+
completeInstructions,
|
|
100
101
|
conversationKey ?? "implicit-thread",
|
|
101
102
|
)
|
|
102
103
|
: null;
|
|
@@ -113,6 +113,29 @@ export function formatThinkingSummaryContent(delta: any): string {
|
|
|
113
113
|
return sections.join("\n\n");
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Matches thinking phases verified in upstream Qwen Web HAR:
|
|
118
|
+
* - "thinking_summary": structured thinking in Qwen Max / Plus
|
|
119
|
+
* - "think": incremental thinking deltas in Qwen Omni
|
|
120
|
+
*/
|
|
121
|
+
export function isThinkingPhase(phase: unknown): boolean {
|
|
122
|
+
return phase === "think" || phase === "thinking_summary";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Extract thinking/reasoning text from a delta chunk, supporting both
|
|
127
|
+
* structured summaries (summary_title/summary_thought) and direct content deltas.
|
|
128
|
+
*/
|
|
129
|
+
export function extractThinkingContent(delta: any): string {
|
|
130
|
+
if (
|
|
131
|
+
Array.isArray(delta?.extra?.summary_title?.content) ||
|
|
132
|
+
Array.isArray(delta?.extra?.summary_thought?.content)
|
|
133
|
+
) {
|
|
134
|
+
return formatThinkingSummaryContent(delta);
|
|
135
|
+
}
|
|
136
|
+
return typeof delta?.content === "string" ? delta.content : "";
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
export function isAbortError(err: unknown): boolean {
|
|
117
140
|
if (err instanceof DOMException) {
|
|
118
141
|
return err.name === "AbortError";
|
|
@@ -67,6 +67,8 @@ import {
|
|
|
67
67
|
} from "../../services/context-meter.ts";
|
|
68
68
|
import {
|
|
69
69
|
getIncrementalDelta,
|
|
70
|
+
isThinkingPhase,
|
|
71
|
+
extractThinkingContent,
|
|
70
72
|
formatThinkingSummaryContent,
|
|
71
73
|
shouldSuppressStreamAbort,
|
|
72
74
|
isAbortError,
|
|
@@ -436,9 +438,9 @@ export async function processNonStreamingResponse(
|
|
|
436
438
|
) {
|
|
437
439
|
const delta = chunk.choices[0].delta;
|
|
438
440
|
|
|
439
|
-
if (delta.phase
|
|
441
|
+
if (isThinkingPhase(delta.phase)) {
|
|
440
442
|
isThinkingChunk = true;
|
|
441
|
-
const formattedSummary =
|
|
443
|
+
const formattedSummary = extractThinkingContent(delta);
|
|
442
444
|
if (formattedSummary) {
|
|
443
445
|
const result = getIncrementalDelta(
|
|
444
446
|
lastThinkingSummary,
|
|
@@ -1737,9 +1739,9 @@ export async function processStreamingResponse(
|
|
|
1737
1739
|
break; // Exit the for loop; the while check leaves the read loop
|
|
1738
1740
|
}
|
|
1739
1741
|
|
|
1740
|
-
if (delta.phase
|
|
1742
|
+
if (isThinkingPhase(delta.phase)) {
|
|
1741
1743
|
isThinkingChunk = true;
|
|
1742
|
-
const formattedSummary =
|
|
1744
|
+
const formattedSummary = extractThinkingContent(delta);
|
|
1743
1745
|
if (formattedSummary) {
|
|
1744
1746
|
const result = getIncrementalDelta(
|
|
1745
1747
|
lastThinkingSummary,
|
|
@@ -2254,9 +2256,9 @@ export async function processStreamingResponse(
|
|
|
2254
2256
|
let foundStr = false;
|
|
2255
2257
|
let isThinkingChunk = false;
|
|
2256
2258
|
|
|
2257
|
-
if (delta.phase
|
|
2259
|
+
if (isThinkingPhase(delta.phase)) {
|
|
2258
2260
|
isThinkingChunk = true;
|
|
2259
|
-
const formattedSummary =
|
|
2261
|
+
const formattedSummary = extractThinkingContent(delta);
|
|
2260
2262
|
if (formattedSummary) {
|
|
2261
2263
|
const result = getIncrementalDelta(
|
|
2262
2264
|
lastThinkingSummary,
|
|
@@ -2655,10 +2657,29 @@ export async function processStreamingResponse(
|
|
|
2655
2657
|
} else {
|
|
2656
2658
|
logger.error("[Chat] Stream callback error", errorDetails);
|
|
2657
2659
|
}
|
|
2658
|
-
|
|
2659
|
-
//
|
|
2660
|
-
//
|
|
2660
|
+
// The HTTP response is already committed at this point.
|
|
2661
|
+
// 1. Emit an explicit assistant message delta so CLI/TUI clients that only
|
|
2662
|
+
// listen for choices[].delta.content (e.g. OpenCode, Claude Code, etc.)
|
|
2663
|
+
// render the error visibly in the chat instead of appearing blank/frozen.
|
|
2664
|
+
// 2. Emit a terminal OpenAI-compatible SSE error event + [DONE].
|
|
2661
2665
|
try {
|
|
2666
|
+
const userFriendlyNotice = `\n\n⚠️ **[Qwen Security / Erro]** ${err.message || "A resposta foi interrompida pelo Qwen."}\n\n`;
|
|
2667
|
+
const errorDelta = {
|
|
2668
|
+
id: completionId,
|
|
2669
|
+
object: "chat.completion.chunk",
|
|
2670
|
+
created: Math.floor(Date.now() / 1000),
|
|
2671
|
+
model: body.model,
|
|
2672
|
+
choices: [
|
|
2673
|
+
{
|
|
2674
|
+
index: 0,
|
|
2675
|
+
delta: { content: userFriendlyNotice },
|
|
2676
|
+
logprobs: null,
|
|
2677
|
+
finish_reason: "stop",
|
|
2678
|
+
},
|
|
2679
|
+
],
|
|
2680
|
+
};
|
|
2681
|
+
|
|
2682
|
+
await errorStream.write(`data: ${JSON.stringify(errorDelta)}\n\n`);
|
|
2662
2683
|
await errorStream.write(
|
|
2663
2684
|
`data: ${JSON.stringify({
|
|
2664
2685
|
error: {
|
|
@@ -2668,7 +2689,7 @@ export async function processStreamingResponse(
|
|
|
2668
2689
|
},
|
|
2669
2690
|
})}\n\ndata: [DONE]\n\n`,
|
|
2670
2691
|
);
|
|
2671
|
-
} catch
|
|
2692
|
+
} catch {
|
|
2672
2693
|
// Stream already closed — client already disconnected or the stream
|
|
2673
2694
|
// was cancelled. Nothing more we can do.
|
|
2674
2695
|
}
|
|
@@ -43,14 +43,7 @@ export async function parseRequestBody(c: Context): Promise<ParsedRequest> {
|
|
|
43
43
|
const body: OpenAIRequest = await c.req.json();
|
|
44
44
|
logIncomingChatRequest(c, body);
|
|
45
45
|
const isStream = body.stream ?? false;
|
|
46
|
-
const conversationKey =
|
|
47
|
-
typeof body.session_id === "string" && body.session_id.trim().length > 0
|
|
48
|
-
? body.session_id.trim()
|
|
49
|
-
: typeof body.conversation_id === "string" &&
|
|
50
|
-
body.conversation_id.trim().length > 0
|
|
51
|
-
? body.conversation_id.trim()
|
|
52
|
-
: null;
|
|
53
|
-
|
|
46
|
+
const conversationKey = extractExplicitConversationKey(c, body);
|
|
54
47
|
const messages = body.messages || [];
|
|
55
48
|
let uploadHeaders: Record<string, string> | null = null;
|
|
56
49
|
|
|
@@ -328,6 +321,63 @@ function contentLength(value: unknown): number {
|
|
|
328
321
|
return 0;
|
|
329
322
|
}
|
|
330
323
|
|
|
324
|
+
export function extractExplicitConversationKey(
|
|
325
|
+
c: Context,
|
|
326
|
+
body: OpenAIRequest,
|
|
327
|
+
): string | null {
|
|
328
|
+
const b = body as any;
|
|
329
|
+
|
|
330
|
+
// 1. Direct body properties (OpenAI-compatible extensions)
|
|
331
|
+
if (typeof b?.session_id === "string" && b.session_id.trim().length > 0) {
|
|
332
|
+
return b.session_id.trim();
|
|
333
|
+
}
|
|
334
|
+
if (typeof b?.conversation_id === "string" && b.conversation_id.trim().length > 0) {
|
|
335
|
+
return b.conversation_id.trim();
|
|
336
|
+
}
|
|
337
|
+
if (typeof b?.chat_id === "string" && b.chat_id.trim().length > 0) {
|
|
338
|
+
return b.chat_id.trim();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 2. HTTP headers sent natively by coding agents
|
|
342
|
+
const headerCandidates = [
|
|
343
|
+
c.req.header("x-session-id"), // OpenCode, Cline, AI-SDK, custom
|
|
344
|
+
c.req.header("x-session-affinity"), // OpenCode / AI-SDK
|
|
345
|
+
c.req.header("session-id"), // OpenAI Codex CLI
|
|
346
|
+
c.req.header("x-claude-code-session-id"), // Claude Code CLI
|
|
347
|
+
c.req.header("x-conversation-id"), // Standard agents
|
|
348
|
+
c.req.header("conversation-id"), // Standard agents
|
|
349
|
+
c.req.header("x-client-request-id"), // Client fallback ID
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
for (const candidate of headerCandidates) {
|
|
353
|
+
if (typeof candidate === "string" && candidate.trim().length > 0) {
|
|
354
|
+
return candidate.trim();
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 3. Metadata payloads (Claude Code embeds session_id in user_id metadata JSON)
|
|
359
|
+
if (b?.metadata && typeof b.metadata === "object") {
|
|
360
|
+
if (typeof b.metadata.session_id === "string" && b.metadata.session_id.trim().length > 0) {
|
|
361
|
+
return b.metadata.session_id.trim();
|
|
362
|
+
}
|
|
363
|
+
if (typeof b.metadata.user_id === "string" && b.metadata.user_id.includes('"session_id":')) {
|
|
364
|
+
try {
|
|
365
|
+
const parsed = JSON.parse(b.metadata.user_id);
|
|
366
|
+
if (typeof parsed?.session_id === "string" && parsed.session_id.trim().length > 0) {
|
|
367
|
+
return parsed.session_id.trim();
|
|
368
|
+
}
|
|
369
|
+
} catch {}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 4. Prompt cache key (used by Codex for session consistency)
|
|
374
|
+
if (typeof b?.prompt_cache_key === "string" && b.prompt_cache_key.trim().length > 0) {
|
|
375
|
+
return b.prompt_cache_key.trim();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
|
|
331
381
|
function logIncomingChatRequest(c: Context, body: OpenAIRequest): void {
|
|
332
382
|
const messages = Array.isArray(body.messages) ? body.messages : [];
|
|
333
383
|
const tools = Array.isArray((body as any).tools) ? (body as any).tools : [];
|
|
@@ -65,11 +65,16 @@ app.post("/v1/responses", async (c) => {
|
|
|
65
65
|
historyMessages = history;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const responsesSessionId =
|
|
69
|
+
c.req.header("session-id") ||
|
|
70
|
+
c.req.header("x-session-id") ||
|
|
71
|
+
c.req.header("x-client-request-id") ||
|
|
72
|
+
(req as any).prompt_cache_key ||
|
|
73
|
+
(typeof (body as any)?.session_id === "string" ? (body as any).session_id : undefined);
|
|
68
74
|
// Convert to Chat Completions format
|
|
69
75
|
const chatRequest = responsesToChatCompletions(req, historyMessages);
|
|
70
76
|
|
|
71
77
|
if (isStream) {
|
|
72
|
-
// ============ STREAMING MODE ============
|
|
73
78
|
const socket =
|
|
74
79
|
(c.env as any)?.incoming?.socket || (c.req.raw as any)?.socket;
|
|
75
80
|
if (socket && typeof socket.setNoDelay === "function") {
|
|
@@ -136,9 +141,14 @@ app.post("/v1/responses", async (c) => {
|
|
|
136
141
|
"Content-Type": "application/json",
|
|
137
142
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
138
143
|
"x-qwenproxy-route": "Responses",
|
|
144
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
145
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
146
|
+
: {}),
|
|
147
|
+
...(responsesSessionId ? { "session-id": responsesSessionId, "x-session-id": responsesSessionId } : {}),
|
|
139
148
|
},
|
|
140
149
|
body: JSON.stringify({
|
|
141
150
|
...chatRequest,
|
|
151
|
+
...(responsesSessionId ? { session_id: responsesSessionId } : {}),
|
|
142
152
|
stream: true,
|
|
143
153
|
stream_options: { include_usage: true },
|
|
144
154
|
}),
|
|
@@ -300,19 +310,17 @@ app.post("/v1/responses", async (c) => {
|
|
|
300
310
|
"Content-Type": "application/json",
|
|
301
311
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
302
312
|
"x-qwenproxy-route": "Responses",
|
|
313
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
314
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
315
|
+
: {}),
|
|
316
|
+
...(responsesSessionId ? { "session-id": responsesSessionId, "x-session-id": responsesSessionId } : {}),
|
|
303
317
|
},
|
|
304
|
-
body: JSON.stringify(
|
|
318
|
+
body: JSON.stringify({
|
|
319
|
+
...chatRequest,
|
|
320
|
+
...(responsesSessionId ? { session_id: responsesSessionId } : {}),
|
|
321
|
+
}),
|
|
305
322
|
},
|
|
306
323
|
);
|
|
307
|
-
|
|
308
|
-
if (!response.ok) {
|
|
309
|
-
const errorText = await response.text();
|
|
310
|
-
console.error(
|
|
311
|
-
`[Responses] Upstream error: ${response.status} ${errorText}`,
|
|
312
|
-
);
|
|
313
|
-
return responsesError(c, "api_error", "Upstream service error", 502);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
324
|
const chatResponse = await response.json();
|
|
317
325
|
const responsesResponse = chatCompletionsToResponses(
|
|
318
326
|
chatResponse,
|
|
@@ -61,7 +61,7 @@ import { loadAccounts, type QwenAccount } from "../core/accounts.ts";
|
|
|
61
61
|
// acyclic, while the reverse direction would drag the browser layer into core.
|
|
62
62
|
import { hasActiveAccountLease } from "../core/account-concurrency.ts";
|
|
63
63
|
import { config } from "../core/config.ts";
|
|
64
|
-
import { maskEmail } from "../core/logger.ts";
|
|
64
|
+
import { maskEmail, logger } from "../core/logger.ts";
|
|
65
65
|
import { Mutex } from "../core/mutex.ts";
|
|
66
66
|
import {
|
|
67
67
|
markAccountHeadersReady,
|
|
@@ -184,6 +184,22 @@ async function recoverStuckAccountMutex(
|
|
|
184
184
|
// account be initialized again instead of remaining permanently wedged.
|
|
185
185
|
if (accountMutexes.get(accountId) !== mutex) return;
|
|
186
186
|
|
|
187
|
+
const lockState = mutex.state();
|
|
188
|
+
// A lock is only considered stuck if it has been held for at least PLAYWRIGHT_MUTEX_WAIT_MS (60s).
|
|
189
|
+
// A waiter with a short timeout (e.g. 5s) timing out does NOT mean the lock holder is stuck!
|
|
190
|
+
// Furthermore, never nuke an account while it is initializing (init: takes 20-30s)
|
|
191
|
+
// or while it is actively serving a stream to a user.
|
|
192
|
+
if (
|
|
193
|
+
lockState.heldForMs < PLAYWRIGHT_MUTEX_WAIT_MS ||
|
|
194
|
+
lockState.heldBy.startsWith("init:") ||
|
|
195
|
+
isAccountServingStream(accountId)
|
|
196
|
+
) {
|
|
197
|
+
logger.warn(
|
|
198
|
+
`[Playwright] Skipping destructive mutex recovery | account=${accountId} | heldBy=${lockState.heldBy} | heldFor=${lockState.heldForMs}ms | limit=${PLAYWRIGHT_MUTEX_WAIT_MS}ms | servingStream=${isAccountServingStream(accountId)}`,
|
|
199
|
+
);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
187
203
|
console.warn(
|
|
188
204
|
`[Playwright] Recovering stuck account mutex | account=${accountId} | key=${key}`,
|
|
189
205
|
);
|
|
@@ -3353,6 +3369,10 @@ export function isPlaywrightInitialized(accountId: string): boolean {
|
|
|
3353
3369
|
return accountPages.has(accountId);
|
|
3354
3370
|
}
|
|
3355
3371
|
|
|
3372
|
+
export function isPlaywrightInitializing(accountId: string): boolean {
|
|
3373
|
+
return inFlightAccountInits.has(accountId);
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3356
3376
|
export function isAccountRecentlyActive(
|
|
3357
3377
|
accountId: string,
|
|
3358
3378
|
maxIdleMs = 300_000,
|
package/src/services/qwen.ts
CHANGED
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
syncModelMetadata,
|
|
28
28
|
} from "../core/model-registry.ts";
|
|
29
29
|
import { type Page, type BrowserContext } from "patchright";
|
|
30
|
-
import { withAccountPage, assertAntiBotHeaders, onBrowserContextCreated } from "./playwright.ts";
|
|
30
|
+
import { withAccountPage, assertAntiBotHeaders, onBrowserContextCreated, isPlaywrightInitializing } from "./playwright.ts";
|
|
31
31
|
import { recoverBaxiaCaptcha } from "./captcha-coordinator.ts";
|
|
32
32
|
import { startBaxiaCaptchaWatcher } from "./captcha-solver.ts";
|
|
33
33
|
import { isAccountBusy } from "../core/account-concurrency.ts";
|
|
@@ -795,7 +795,7 @@ async function withQwenBrowserPage<T>(
|
|
|
795
795
|
return fn(page);
|
|
796
796
|
},
|
|
797
797
|
operationTimeoutMs,
|
|
798
|
-
Math.min(config.timeouts.page,
|
|
798
|
+
Math.min(config.timeouts.page, 30_000),
|
|
799
799
|
recoverOnTimeout,
|
|
800
800
|
);
|
|
801
801
|
}
|
|
@@ -1767,6 +1767,14 @@ export async function disableNativeTools(accountId?: string): Promise<void> {
|
|
|
1767
1767
|
) {
|
|
1768
1768
|
return;
|
|
1769
1769
|
}
|
|
1770
|
+
// Defer if the account's Playwright browser is still initializing: the
|
|
1771
|
+
// settings POST requires the page mutex, and init legitimately holds it for
|
|
1772
|
+
// 20-30s (navigation + bx SDK + header capture). Attempting to acquire the
|
|
1773
|
+
// mutex now would time out (5s default in withQwenBrowserPage) and log a
|
|
1774
|
+
// spurious WARN. The startup flow calls us again after init finishes.
|
|
1775
|
+
if (accountId && isPlaywrightInitializing(accountId)) {
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1770
1778
|
disablingNativeToolsInProgress.add(cacheKey);
|
|
1771
1779
|
|
|
1772
1780
|
try {
|
|
@@ -2827,7 +2835,7 @@ async function createQwenStreamInternal(
|
|
|
2827
2835
|
auto_thinking: mode === "auto",
|
|
2828
2836
|
thinking_mode: thinkingMode,
|
|
2829
2837
|
...(thinkingEnabled ? { thinking_format: "summary" } : {}),
|
|
2830
|
-
auto_search:
|
|
2838
|
+
auto_search: false,
|
|
2831
2839
|
};
|
|
2832
2840
|
})(),
|
|
2833
2841
|
extra: {
|
package/src/tools/parser.ts
CHANGED
|
@@ -1703,6 +1703,12 @@ export class StreamingToolParser {
|
|
|
1703
1703
|
result.text += literalBlock;
|
|
1704
1704
|
}
|
|
1705
1705
|
|
|
1706
|
+
// Count undeclared/malformed tool calls toward the per-turn cap.
|
|
1707
|
+
// Without this, the model can generate unlimited undeclared tool calls
|
|
1708
|
+
// (e.g. Qwen-native WebSearch/WebFetch) that bypass the cap entirely,
|
|
1709
|
+
// causing infinite generation until TOTAL_REQUEST_TIMEOUT (10 min).
|
|
1710
|
+
this.emittedToolCallCount++;
|
|
1711
|
+
|
|
1706
1712
|
this.advanceMarkdownState(literalBlock);
|
|
1707
1713
|
this.pendingLeadIn = "";
|
|
1708
1714
|
}
|