qwenproxy-cli 1.0.16 → 1.0.17
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/api/server.ts +39 -21
- package/src/routes/chat/validation.ts +14 -9
- package/src/tools/parser.ts +55 -4
- package/src/utils/json.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.17",
|
|
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": {
|
package/src/api/server.ts
CHANGED
|
@@ -653,16 +653,12 @@ export async function startServer(options?: {
|
|
|
653
653
|
const totalAccounts = accounts.length;
|
|
654
654
|
|
|
655
655
|
// Warm accounts in priority order (recently successful accounts first),
|
|
656
|
-
// skipping accounts still on cooldown. Warm
|
|
657
|
-
//
|
|
658
|
-
//
|
|
656
|
+
// skipping accounts still on cooldown. Warm the primary account first so
|
|
657
|
+
// the server binds the port and goes online immediately (~15-20s).
|
|
658
|
+
// Reserve account(s) and standby validations run seamlessly in background.
|
|
659
659
|
const warmOrder = getAccountsByPriority(accounts).filter(
|
|
660
660
|
(account) => !getAccountCooldownInfo(account.id),
|
|
661
661
|
);
|
|
662
|
-
const targetWarmCount = Math.min(
|
|
663
|
-
warmOrder.length,
|
|
664
|
-
Math.max(1, config.playwright.maxActiveContexts),
|
|
665
|
-
);
|
|
666
662
|
const readyAccountIds = new Set<string>();
|
|
667
663
|
|
|
668
664
|
for (let i = 0; i < warmOrder.length; i++) {
|
|
@@ -676,11 +672,9 @@ export async function startServer(options?: {
|
|
|
676
672
|
if (ok) {
|
|
677
673
|
readyAccountIds.add(warmOrder[i].id);
|
|
678
674
|
console.log(
|
|
679
|
-
`✅ [Server] Account ready (
|
|
675
|
+
`✅ [Server] Account ready (1/${totalAccounts}): ${maskEmail(warmOrder[i].email)}`,
|
|
680
676
|
);
|
|
681
|
-
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
677
|
+
break;
|
|
684
678
|
}
|
|
685
679
|
}
|
|
686
680
|
|
|
@@ -717,16 +711,43 @@ export async function startServer(options?: {
|
|
|
717
711
|
`🪶 [Server] ${remainingAccounts.length} standby account(s) will initialize on demand`,
|
|
718
712
|
);
|
|
719
713
|
|
|
720
|
-
//
|
|
721
|
-
//
|
|
714
|
+
// In background: warm 1 reserve account (if maxActiveContexts > 1) and
|
|
715
|
+
// validate the rest of the standby accounts
|
|
722
716
|
void (async () => {
|
|
723
717
|
const { validateAccountLogin } = await import("../services/playwright.ts");
|
|
724
718
|
const { ensureAccountInPriority } = await import("../core/account-priority.ts");
|
|
725
719
|
|
|
720
|
+
let accountsToValidate = remainingAccounts;
|
|
721
|
+
|
|
722
|
+
// Warm reserve account in background for fast failover without delaying startup
|
|
723
|
+
if (config.playwright.maxActiveContexts > 1 && remainingAccounts.length > 0) {
|
|
724
|
+
const reserveAccount = remainingAccounts[0];
|
|
725
|
+
accountsToValidate = remainingAccounts.slice(1);
|
|
726
|
+
try {
|
|
727
|
+
const ok = await prepareAccountRuntime(
|
|
728
|
+
reserveAccount,
|
|
729
|
+
getAccountCredentials,
|
|
730
|
+
initPlaywrightForAccount,
|
|
731
|
+
disableNativeTools,
|
|
732
|
+
warmQwenChatPool,
|
|
733
|
+
);
|
|
734
|
+
if (ok) {
|
|
735
|
+
ensureAccountInPriority(reserveAccount.id);
|
|
736
|
+
console.log(
|
|
737
|
+
`✅ [Server] Reserve account ready (2/${totalAccounts}): ${maskEmail(reserveAccount.email)}`,
|
|
738
|
+
);
|
|
739
|
+
}
|
|
740
|
+
} catch (err) {
|
|
741
|
+
console.warn(
|
|
742
|
+
`⚠️ [Server] Failed to warm reserve account ${maskEmail(reserveAccount.email)}: ${getErrorMessage(err)}`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
726
747
|
let validated = 0;
|
|
727
748
|
let failed = 0;
|
|
728
749
|
|
|
729
|
-
for (const account of
|
|
750
|
+
for (const account of accountsToValidate) {
|
|
730
751
|
try {
|
|
731
752
|
const creds = getAccountCredentials(account.id) ?? account;
|
|
732
753
|
// Validate login in background with real unmasked credentials
|
|
@@ -757,17 +778,14 @@ export async function startServer(options?: {
|
|
|
757
778
|
markAccountRateLimited(
|
|
758
779
|
account.id,
|
|
759
780
|
24 * 3600 * 1000,
|
|
760
|
-
`
|
|
781
|
+
`StandbyValidationError: ${getErrorMessage(error)}`,
|
|
761
782
|
);
|
|
762
783
|
}
|
|
763
784
|
}
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
`⚠️ [Server] Standby validation finished: ${validated} ok, ${failed} failed`,
|
|
767
|
-
);
|
|
768
|
-
} else if (validated > 0) {
|
|
785
|
+
|
|
786
|
+
if (validated > 0 || failed > 0) {
|
|
769
787
|
console.log(
|
|
770
|
-
`✅ [Server] Standby validation complete:
|
|
788
|
+
`✅ [Server] Standby validation complete: ${validated} account(s) ready${failed > 0 ? `, ${failed} failed` : ""}`,
|
|
771
789
|
);
|
|
772
790
|
}
|
|
773
791
|
})().catch((error) => {
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "../../core/reasoning-effort.ts";
|
|
19
19
|
|
|
20
20
|
import { TOOL_CALL_OPEN, TOOL_CALL_CLOSE } from "../../tools/toolcall-tags.ts";
|
|
21
|
+
import { robustParseJSON } from "../../utils/json.ts";
|
|
21
22
|
|
|
22
23
|
export interface ParsedRequest {
|
|
23
24
|
body: OpenAIRequest;
|
|
@@ -224,15 +225,19 @@ async function buildPromptFromMessages(
|
|
|
224
225
|
if (typeof args === "string") {
|
|
225
226
|
try {
|
|
226
227
|
parsedArgs = JSON.parse(args);
|
|
227
|
-
} catch
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
228
|
+
} catch {
|
|
229
|
+
try {
|
|
230
|
+
parsedArgs = robustParseJSON(args);
|
|
231
|
+
} catch (parseErr) {
|
|
232
|
+
// Malformed JSON: preserve raw string for model visibility
|
|
233
|
+
logger.warn("[chat] Failed to parse tool_call arguments", {
|
|
234
|
+
toolCallId: tc.id,
|
|
235
|
+
toolName: tc.function?.name,
|
|
236
|
+
error: parseErr instanceof Error ? parseErr.message : "Unknown",
|
|
237
|
+
rawArgs: args.substring(0, 200),
|
|
238
|
+
});
|
|
239
|
+
parsedArgs = { _raw: args };
|
|
240
|
+
}
|
|
236
241
|
}
|
|
237
242
|
} else if (args && typeof args === "object") {
|
|
238
243
|
parsedArgs = args;
|
package/src/tools/parser.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
getOpenNames,
|
|
10
10
|
getCloseNames,
|
|
11
11
|
matchToolCloseAt,
|
|
12
|
+
stripTrailingStrayCloses,
|
|
12
13
|
} from "./toolcall-tags.ts";
|
|
13
14
|
|
|
14
15
|
export interface ToolCallDelta {
|
|
@@ -190,6 +191,45 @@ function closeTagContentIsParseable(buffer: string, endIdx: number): boolean {
|
|
|
190
191
|
return tryParseJsonToolPayload(content);
|
|
191
192
|
}
|
|
192
193
|
|
|
194
|
+
function balanceClosingBrackets(content: string): string {
|
|
195
|
+
let inString = false;
|
|
196
|
+
let escaped = false;
|
|
197
|
+
const stack: string[] = [];
|
|
198
|
+
for (let i = 0; i < content.length; i++) {
|
|
199
|
+
const ch = content[i];
|
|
200
|
+
if (inString) {
|
|
201
|
+
if (escaped) {
|
|
202
|
+
escaped = false;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
if (ch === "\\") {
|
|
206
|
+
escaped = true;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (ch === '"') inString = false;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (ch === '"') {
|
|
213
|
+
inString = true;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (ch === "{" || ch === "[") stack.push(ch);
|
|
217
|
+
else if (ch === "}" || ch === "]") {
|
|
218
|
+
const top = stack[stack.length - 1];
|
|
219
|
+
if ((top === "{" && ch === "}") || (top === "[" && ch === "]")) {
|
|
220
|
+
stack.pop();
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (inString) return content;
|
|
225
|
+
let out = content;
|
|
226
|
+
while (stack.length > 0) {
|
|
227
|
+
const open = stack.pop();
|
|
228
|
+
out += open === "{" ? "}" : "]";
|
|
229
|
+
}
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
|
|
193
233
|
/**
|
|
194
234
|
* Plain-JSON.parse based candidate checks, in increasing tolerance order:
|
|
195
235
|
* raw payload -> narrow typo repairs -> doubled trailing brace/bracket ->
|
|
@@ -214,9 +254,13 @@ function tryParseJsonToolPayload(content: string): boolean {
|
|
|
214
254
|
|
|
215
255
|
const candidates = [repaired, stripped];
|
|
216
256
|
if (repaired !== content) candidates.push(strippedRepaired);
|
|
217
|
-
candidates.push(`{
|
|
218
|
-
if (repaired !== content) candidates.push(`{
|
|
257
|
+
candidates.push(`{"${content}`, `{${content}`);
|
|
258
|
+
if (repaired !== content) candidates.push(`{"${repaired}`, `{${repaired}`);
|
|
219
259
|
|
|
260
|
+
const balanced = balanceClosingBrackets(content);
|
|
261
|
+
if (balanced !== content) candidates.push(balanced);
|
|
262
|
+
const balancedRepaired = balanceClosingBrackets(repaired);
|
|
263
|
+
if (balancedRepaired !== content) candidates.push(balancedRepaired);
|
|
220
264
|
return candidates.some((candidate) => tryParse(candidate));
|
|
221
265
|
}
|
|
222
266
|
|
|
@@ -999,6 +1043,10 @@ function isJsonPayloadTruncated(content: string): boolean {
|
|
|
999
1043
|
if (content.includes('\\"')) {
|
|
1000
1044
|
alt.push(content.replace(/\\"/g, '"'));
|
|
1001
1045
|
}
|
|
1046
|
+
const balanced = balanceClosingBrackets(trimmed);
|
|
1047
|
+
if (balanced !== trimmed) {
|
|
1048
|
+
alt.push(balanced);
|
|
1049
|
+
}
|
|
1002
1050
|
for (const candidate of alt) {
|
|
1003
1051
|
if (!scanJsonStructureIncomplete(candidate)) return false;
|
|
1004
1052
|
}
|
|
@@ -1864,7 +1912,7 @@ export class StreamingToolParser {
|
|
|
1864
1912
|
// argument values (e.g. `{"a": "1</tool_call>"}`). Genuine unclosed
|
|
1865
1913
|
// streams (cut mid-payload) have no trailing tag, so this is a no-op
|
|
1866
1914
|
// for them.
|
|
1867
|
-
const trimmed = rawTrimmed.
|
|
1915
|
+
const trimmed = stripTrailingStrayCloses(rawTrimmed).trim();
|
|
1868
1916
|
if (trimmed.length > 0) {
|
|
1869
1917
|
if (isToolcallDebugEnabled()) {
|
|
1870
1918
|
logger.debug(
|
|
@@ -2570,10 +2618,13 @@ export class StreamingToolParser {
|
|
|
2570
2618
|
// malformed tracking fires and the model re-emits cleanly.
|
|
2571
2619
|
if (isJsonPayloadTruncated(block)) return null;
|
|
2572
2620
|
const variants = [block];
|
|
2621
|
+
const balanced = balanceClosingBrackets(block);
|
|
2622
|
+
if (balanced !== block) {
|
|
2623
|
+
variants.push(balanced);
|
|
2624
|
+
}
|
|
2573
2625
|
if (block.includes('\\"')) {
|
|
2574
2626
|
variants.push(block.replace(/\\"/g, '"'));
|
|
2575
2627
|
}
|
|
2576
|
-
|
|
2577
2628
|
for (const variant of variants) {
|
|
2578
2629
|
try {
|
|
2579
2630
|
const parsed = robustParseJSON(variant);
|
package/src/utils/json.ts
CHANGED
|
@@ -191,6 +191,13 @@ export function robustParseJSON(str: string): any {
|
|
|
191
191
|
jsonPart = jsonPart.replace(/\\\\"/g, '\\"');
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
// Heal stray backslashes escaping quotes on property keys or values
|
|
195
|
+
// e.g. "key\": \"val\" or "key\": "val" -> "key": "val"
|
|
196
|
+
jsonPart = jsonPart
|
|
197
|
+
.replace(/([{,]\s*)"([a-zA-Z0-9_-]+)\\"(\s*:)/g, '$1"$2"$3')
|
|
198
|
+
.replace(/:\s*\\"([^"\\]*)\\"(\s*[,}\]])/g, ': "$1"$2')
|
|
199
|
+
.replace(/:\s*\\"([^"\\]*)("(?:\s*[,}\]]))/g, ': "$1$2')
|
|
200
|
+
.replace(/:\s*"([^"\\]*)\\"(\s*[,}\]])/g, ': "$1"$2');
|
|
194
201
|
let currentJson = jsonPart.replace(
|
|
195
202
|
/([{,]\s*)([a-zA-Z_][a-zA-Z0-9_]*)(\s*:)/g,
|
|
196
203
|
'$1"$2"$3',
|