qwenproxy-cli 1.2.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qwenproxy-cli",
3
- "version": "1.2.1",
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: only generate a persistent session ID when we have
91
- // an explicit conversation key. Otherwise, generate an ephemeral ID for
92
- // logging/metrics only (not used for thread reuse). Temp mode never persists
93
- // a thread, so it has no session id.
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
- conversationKey ? completeInstructions : "",
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
  },
@@ -3353,6 +3353,10 @@ export function isPlaywrightInitialized(accountId: string): boolean {
3353
3353
  return accountPages.has(accountId);
3354
3354
  }
3355
3355
 
3356
+ export function isPlaywrightInitializing(accountId: string): boolean {
3357
+ return inFlightAccountInits.has(accountId);
3358
+ }
3359
+
3356
3360
  export function isAccountRecentlyActive(
3357
3361
  accountId: string,
3358
3362
  maxIdleMs = 300_000,
@@ -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: true,
2838
+ auto_search: false,
2831
2839
  };
2832
2840
  })(),
2833
2841
  extra: {
@@ -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
  }