qwenproxy-cli 1.2.0 → 1.2.2
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 +3 -0
- package/src/routes/chat/context.ts +6 -5
- package/src/routes/responses/index.ts +6 -0
- package/src/services/captcha-coordinator.ts +1 -1
- package/src/services/playwright.ts +26 -11
- package/src/services/qwen.ts +10 -2
- package/src/tools/parser.ts +22 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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": {
|
|
@@ -105,6 +105,9 @@ app.post("/v1/messages", async (c) => {
|
|
|
105
105
|
"Content-Type": "application/json",
|
|
106
106
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
107
107
|
"x-qwenproxy-route": "Anthropic",
|
|
108
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
109
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
110
|
+
: {}),
|
|
108
111
|
},
|
|
109
112
|
body: JSON.stringify({
|
|
110
113
|
...openaiRequest,
|
|
@@ -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;
|
|
@@ -136,6 +136,9 @@ app.post("/v1/responses", async (c) => {
|
|
|
136
136
|
"Content-Type": "application/json",
|
|
137
137
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
138
138
|
"x-qwenproxy-route": "Responses",
|
|
139
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
140
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
141
|
+
: {}),
|
|
139
142
|
},
|
|
140
143
|
body: JSON.stringify({
|
|
141
144
|
...chatRequest,
|
|
@@ -300,6 +303,9 @@ app.post("/v1/responses", async (c) => {
|
|
|
300
303
|
"Content-Type": "application/json",
|
|
301
304
|
Authorization: `Bearer ${process.env.API_KEY || config.apiKey || ""}`,
|
|
302
305
|
"x-qwenproxy-route": "Responses",
|
|
306
|
+
...(c.req.header("x-qwenproxy-chat-mode")
|
|
307
|
+
? { "x-qwenproxy-chat-mode": c.req.header("x-qwenproxy-chat-mode")! }
|
|
308
|
+
: {}),
|
|
303
309
|
},
|
|
304
310
|
body: JSON.stringify(chatRequest),
|
|
305
311
|
},
|
|
@@ -2335,19 +2335,30 @@ export async function captureQwenHeaders(
|
|
|
2335
2335
|
1,
|
|
2336
2336
|
Math.min(CHAT_INPUT_ACTION_TIMEOUT_MS, remainingBudgetMs()),
|
|
2337
2337
|
);
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
if (settled || page.isClosed()) return;
|
|
2341
|
-
await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
|
|
2338
|
+
let interactionSucceeded = false;
|
|
2339
|
+
for (let interactionTry = 1; interactionTry <= 2; interactionTry++) {
|
|
2342
2340
|
if (settled || page.isClosed()) return;
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2341
|
+
try {
|
|
2342
|
+
await page.focus(inputSelector, { timeout: inputActionTimeoutMs });
|
|
2343
|
+
if (settled || page.isClosed()) return;
|
|
2344
|
+
await page.fill(inputSelector, "", { timeout: inputActionTimeoutMs });
|
|
2345
|
+
if (settled || page.isClosed()) return;
|
|
2346
|
+
await page.type(inputSelector, "a", {
|
|
2347
|
+
delay: 100,
|
|
2348
|
+
timeout: inputActionTimeoutMs,
|
|
2349
|
+
});
|
|
2350
|
+
interactionSucceeded = true;
|
|
2351
|
+
break;
|
|
2352
|
+
} catch {
|
|
2353
|
+
if (settled || page.isClosed()) return;
|
|
2354
|
+
if (interactionTry === 1) {
|
|
2355
|
+
await sleep(300);
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
if (!interactionSucceeded) {
|
|
2348
2360
|
// The input detached mid-interaction (challenge overlay, SPA
|
|
2349
|
-
// re-render):
|
|
2350
|
-
// recovers it.
|
|
2361
|
+
// re-render): only a fresh load recovers it.
|
|
2351
2362
|
if (settled || page.isClosed()) return;
|
|
2352
2363
|
console.warn(
|
|
2353
2364
|
`⏱️ [Playwright] Chat input interaction failed for ${accountId} (attempt ${attempt}); reloading`,
|
|
@@ -3342,6 +3353,10 @@ export function isPlaywrightInitialized(accountId: string): boolean {
|
|
|
3342
3353
|
return accountPages.has(accountId);
|
|
3343
3354
|
}
|
|
3344
3355
|
|
|
3356
|
+
export function isPlaywrightInitializing(accountId: string): boolean {
|
|
3357
|
+
return inFlightAccountInits.has(accountId);
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3345
3360
|
export function isAccountRecentlyActive(
|
|
3346
3361
|
accountId: string,
|
|
3347
3362
|
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";
|
|
@@ -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
|
}
|
|
@@ -2043,18 +2049,23 @@ export class StreamingToolParser {
|
|
|
2043
2049
|
recoveryAttempts: truncRecoveryAttempts,
|
|
2044
2050
|
});
|
|
2045
2051
|
logger.warn(
|
|
2046
|
-
|
|
2047
|
-
{
|
|
2048
|
-
toolName,
|
|
2049
|
-
category: "truncated",
|
|
2050
|
-
contentLength: trimmed.length,
|
|
2051
|
-
content: trimmed.substring(0, 2000),
|
|
2052
|
-
failureReason:
|
|
2053
|
-
"stream ended before tool_call closing tag; content too incomplete to reconstruct",
|
|
2054
|
-
recoveryAttempts: truncRecoveryAttempts,
|
|
2055
|
-
emittedToolCallsSoFar: this.emittedToolCallCount,
|
|
2056
|
-
},
|
|
2052
|
+
`[parser] Dropping unrecoverable unclosed tool call (${toolName || "unknown"}) at end of stream: stream ended before closing tag (${trimmed.length} chars)`,
|
|
2057
2053
|
);
|
|
2054
|
+
if (isToolcallDebugEnabled()) {
|
|
2055
|
+
logger.debug(
|
|
2056
|
+
"[parser] Unclosed tool call payload details",
|
|
2057
|
+
{
|
|
2058
|
+
toolName,
|
|
2059
|
+
category: "truncated",
|
|
2060
|
+
contentLength: trimmed.length,
|
|
2061
|
+
content: trimmed.substring(0, 2000),
|
|
2062
|
+
failureReason:
|
|
2063
|
+
"stream ended before tool_call closing tag; content too incomplete to reconstruct",
|
|
2064
|
+
recoveryAttempts: truncRecoveryAttempts,
|
|
2065
|
+
emittedToolCallsSoFar: this.emittedToolCallCount,
|
|
2066
|
+
},
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2058
2069
|
if (
|
|
2059
2070
|
this.emittedToolCallCount === 0 &&
|
|
2060
2071
|
this.pendingLeadIn.trim().length > 0
|