qwenproxy-cli 1.0.31 → 1.1.0
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/README.es.md +269 -0
- package/README.md +175 -782
- package/README.pt-BR.md +1033 -0
- package/package.json +4 -4
- package/src/api/server.ts +18 -2
- package/src/core/accounts.ts +130 -0
- package/src/core/config.ts +95 -4
- package/src/index.ts +3 -0
- package/src/routes/chat/context.ts +9 -9
- package/src/routes/chat/index.ts +15 -6
- package/src/services/qwen-chat-pool.ts +4 -5
- package/src/services/qwen.ts +2 -5
- package/src/sync/index.ts +124 -80
- package/src/tui/app.ts +8 -9
- package/src/tui/index.ts +3 -0
- package/src/tui/proxy-client.ts +3 -2
- package/src/tui/screen.ts +10 -3
- package/src/tui/settings.ts +2 -0
- package/src/tui/theme.ts +1 -0
- package/src/tui/types.ts +1 -0
- package/src/tui/views/accounts-view.ts +343 -23
- package/src/tui/views/chat-view.ts +209 -14
- package/src/tui/views/status-view.ts +34 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
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": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"!src/tests",
|
|
13
13
|
"!src/benchmarks",
|
|
14
|
-
"README
|
|
14
|
+
"README*.md",
|
|
15
15
|
"LICENSE"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"reset": "npx tsx src/reset-cooldowns.ts",
|
|
27
27
|
"purge": "npx tsx src/delete-chats.ts",
|
|
28
28
|
"test": "npm run test:mock && npm run test:live",
|
|
29
|
-
"test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts",
|
|
30
|
-
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts",
|
|
29
|
+
"test:mock": "tsx --test --test-concurrency=1 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts",
|
|
30
|
+
"test:mock:fast": "tsx --test --test-concurrency=2 --env-file=.env.test src/tests/account-concurrency.test.ts src/tests/concurrency-latest-wins.test.ts src/tests/mutex.test.ts src/tests/context-truncation.test.ts src/tests/streaming-thinking-summary.test.ts src/tests/tool-call-guard.test.ts src/tests/utils-logger-full.test.ts src/tests/logger-toolcall-errors.test.ts src/tests/logger-toolcall-disabled.test.ts src/tests/utils-json-full.test.ts src/tests/utils-session-guard-full.test.ts src/tests/core-logic-full.test.ts src/tests/core-logic-accounts.test.ts src/tests/chat-validation-full.test.ts src/tests/chat-logic-full.test.ts src/tests/account-stickiness.test.ts src/tests/accounts-security.test.ts src/tests/advanced.test.ts src/tests/auth-playwright.test.ts src/tests/captcha-solver.test.ts src/tests/cache-scan-regex.test.ts src/tests/chat-cleanup.test.ts src/tests/chat-cleanup-auto.test.ts src/tests/chromium-args.test.ts src/tests/config-current.test.ts src/tests/context-meter.test.ts src/tests/concurrency.test.ts src/tests/delta.test.ts src/tests/fingerprint.test.ts src/tests/human-behavior.test.ts src/tests/index.test.ts src/tests/json-recovery.test.ts src/tests/jsonFix.test.ts src/tests/memory-cache.test.ts src/tests/model-alias.test.ts src/tests/model-registry-thinking.test.ts src/tests/models-endpoint.test.ts src/tests/multimodal.test.ts src/tests/oss-upload.test.ts src/tests/parser.test.ts src/tests/personalization-flow.test.ts src/tests/personalization-required.test.ts src/tests/retry-policy.test.ts src/tests/rotation.test.ts src/tests/server-lifecycle.test.ts src/tests/session-keeper.test.ts src/tests/session-state.test.ts src/tests/single-account-mode.test.ts src/tests/stream-retry.test.ts src/tests/tool-calls-endpoint.test.ts src/tests/tool-instructions-cache.test.ts src/tests/session-id.test.ts src/tests/watchdog-ram.test.ts src/tests/tool-recovery-t1t2.test.ts src/tests/abort-silent-t4.test.ts src/tests/t3-close-tag-escape.test.ts src/tests/t5-tool-cap.test.ts src/tests/logger-redaction.test.ts src/tests/stall-timeout-retryable.test.ts src/tests/t6-missing-quote-array-close.test.ts src/tests/watchdog-rss.test.ts src/tests/logical-thread-batch.test.ts src/tests/chat-pool-style.test.ts src/tests/queue-wait-cap.test.ts src/tests/context-eviction-order.test.ts src/tests/chat-in-progress-settle.test.ts src/tests/stream-lock-leak.test.ts src/tests/stream-semaphore.test.ts src/tests/waf-isolation.test.ts src/tests/legacy-completions.test.ts src/tests/response-format.test.ts src/tests/cors-headers.test.ts src/tests/stream-lock-terminal-cancel.test.ts src/tests/parser-truncated-tool-call.test.ts src/tests/metrics-prometheus.test.ts src/tests/context-death-cleanup.test.ts src/tests/personalization-direct-fetch.test.ts src/tests/completion-headers-parity.test.ts src/tests/parallel-escape-repro.test.ts src/tests/chat-in-progress-scale-cost.test.ts src/tests/temp-chat-mode.test.ts src/tests/profile-reset.test.ts src/tests/error-classification.test.ts src/tests/quota-cooldown.test.ts src/tests/account-ready-gate.test.ts src/tests/tool-cap-notice.test.ts src/tests/interaction-flows.test.ts src/tests/headers-gate-cooldown-fallback.test.ts src/tests/unwrapped-tool-calls.test.ts src/tests/anthropic.test.ts src/tests/sync-clients.test.ts src/tests/sync-top10-clients.test.ts src/tests/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts src/tests/tui-settings.test.ts src/tests/personalization-deadline.test.ts src/tests/update-cli.test.ts src/tests/batch-accounts.test.ts",
|
|
31
31
|
"test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
|
|
32
32
|
"benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
package/src/api/server.ts
CHANGED
|
@@ -3,7 +3,8 @@ import net from "node:net";
|
|
|
3
3
|
import { v4 as uuidv4 } from "uuid";
|
|
4
4
|
import { Hono, type Context } from "hono";
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
|
-
import { config } from "../core/config.js";
|
|
6
|
+
import { config, getRuntimeChatMode, setRuntimeChatMode } from "../core/config.js";
|
|
7
|
+
import { saveTuiSettings } from "../tui/settings.ts";
|
|
7
8
|
import { metrics } from "../core/metrics.js";
|
|
8
9
|
import { logger, maskEmail } from "../core/logger.js";
|
|
9
10
|
import { MemoryCache } from "../cache/memory-cache.js";
|
|
@@ -248,7 +249,22 @@ app.post("/v1/upload", uploadFile);
|
|
|
248
249
|
app.post("/v1/images/generations", imagesGenerations);
|
|
249
250
|
app.post("/v1/videos/generations", videosGenerations);
|
|
250
251
|
app.get("/v1/tasks/status/:taskId", videoTaskStatus);
|
|
251
|
-
|
|
252
|
+
app.get("/v1/chat/mode", (c) => {
|
|
253
|
+
const error = verifyApiKey(c);
|
|
254
|
+
if (error) return error;
|
|
255
|
+
return c.json({ mode: getRuntimeChatMode() });
|
|
256
|
+
});
|
|
257
|
+
app.post("/v1/chat/mode", async (c) => {
|
|
258
|
+
const error = verifyApiKey(c);
|
|
259
|
+
if (error) return error;
|
|
260
|
+
const body = (await c.req.json().catch(() => ({}))) as { mode?: string };
|
|
261
|
+
if (!body.mode) {
|
|
262
|
+
return c.json({ error: { message: "Field 'mode' is required" } }, 400);
|
|
263
|
+
}
|
|
264
|
+
const updated = setRuntimeChatMode(body.mode);
|
|
265
|
+
saveTuiSettings({ chat: { mode: updated } });
|
|
266
|
+
return c.json({ success: true, mode: updated });
|
|
267
|
+
});
|
|
252
268
|
// OpenAI Responses API compatible routes
|
|
253
269
|
app.route("", responsesApp);
|
|
254
270
|
app.route("", anthropicApp);
|
package/src/core/accounts.ts
CHANGED
|
@@ -122,7 +122,137 @@ export function invalidateAccountsCache(): void {
|
|
|
122
122
|
accountsCache = null;
|
|
123
123
|
accountsCacheTime = 0;
|
|
124
124
|
}
|
|
125
|
+
export interface BatchAccountEntry {
|
|
126
|
+
email: string;
|
|
127
|
+
password: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parseBatchAccounts(rawInput: string): {
|
|
131
|
+
entries: BatchAccountEntry[];
|
|
132
|
+
invalid: string[];
|
|
133
|
+
} {
|
|
134
|
+
if (!rawInput || typeof rawInput !== "string") {
|
|
135
|
+
return { entries: [], invalid: [] };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let text = rawInput.trim();
|
|
139
|
+
// Strip optional QWEN_ACCOUNTS= prefix and surrounding quotes
|
|
140
|
+
text = text.replace(/^QWEN_ACCOUNTS\s*=\s*["']?/i, "").replace(/["']?\s*$/i, "");
|
|
141
|
+
|
|
142
|
+
const lines = text.split(/\r?\n/);
|
|
143
|
+
const entries: BatchAccountEntry[] = [];
|
|
144
|
+
const invalid: string[] = [];
|
|
145
|
+
const seenEmails = new Set<string>();
|
|
146
|
+
|
|
147
|
+
for (const rawLine of lines) {
|
|
148
|
+
const line = rawLine.trim();
|
|
149
|
+
if (!line || line.startsWith("#") || line.startsWith("//")) continue;
|
|
150
|
+
|
|
151
|
+
// Check if line contains multiple accounts separated by comma or semicolon
|
|
152
|
+
const segments =
|
|
153
|
+
(line.includes(",") || line.includes(";")) && (line.match(/@/g) || []).length > 1
|
|
154
|
+
? line.split(/[,;]/).map((s) => s.trim()).filter(Boolean)
|
|
155
|
+
: [line];
|
|
156
|
+
|
|
157
|
+
for (const seg of segments) {
|
|
158
|
+
let email = "";
|
|
159
|
+
let password = "";
|
|
160
|
+
|
|
161
|
+
if (seg.includes("---")) {
|
|
162
|
+
const parts = seg.split("---");
|
|
163
|
+
email = parts[0].trim();
|
|
164
|
+
password = parts.slice(1).join("---").trim();
|
|
165
|
+
} else if (seg.includes("\t")) {
|
|
166
|
+
const parts = seg.split("\t");
|
|
167
|
+
email = parts[0].trim();
|
|
168
|
+
password = parts.slice(1).join("\t").trim();
|
|
169
|
+
} else if (seg.includes(" | ")) {
|
|
170
|
+
const parts = seg.split(" | ");
|
|
171
|
+
email = parts[0].trim();
|
|
172
|
+
password = parts.slice(1).join(" | ").trim();
|
|
173
|
+
} else if (seg.includes(":")) {
|
|
174
|
+
const colonIdx = seg.indexOf(":");
|
|
175
|
+
email = seg.slice(0, colonIdx).trim();
|
|
176
|
+
password = seg.slice(colonIdx + 1).trim();
|
|
177
|
+
} else if (seg.includes(",")) {
|
|
178
|
+
const commaIdx = seg.indexOf(",");
|
|
179
|
+
email = seg.slice(0, commaIdx).trim();
|
|
180
|
+
password = seg.slice(commaIdx + 1).trim();
|
|
181
|
+
} else {
|
|
182
|
+
invalid.push(seg);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Validate email format
|
|
187
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
188
|
+
if (!email || !password || !emailRegex.test(email)) {
|
|
189
|
+
invalid.push(seg);
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const normalizedEmail = email.toLowerCase();
|
|
194
|
+
if (!seenEmails.has(normalizedEmail)) {
|
|
195
|
+
seenEmails.add(normalizedEmail);
|
|
196
|
+
entries.push({ email, password });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { entries, invalid };
|
|
202
|
+
}
|
|
125
203
|
|
|
204
|
+
export function addAccountsBatch(
|
|
205
|
+
entries: BatchAccountEntry[],
|
|
206
|
+
): { added: QwenAccount[]; skipped: string[]; invalid: string[] } {
|
|
207
|
+
const db = getDatabase();
|
|
208
|
+
const added: QwenAccount[] = [];
|
|
209
|
+
const skipped: string[] = [];
|
|
210
|
+
const invalid: string[] = [];
|
|
211
|
+
|
|
212
|
+
const existingEmails = new Set(
|
|
213
|
+
(db.prepare("SELECT email FROM accounts").all() as Array<{ email: string }>).map((r) =>
|
|
214
|
+
r.email.toLowerCase(),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
const insertStmt = db.prepare(
|
|
219
|
+
"INSERT INTO accounts (id, email, password) VALUES (?, ?, ?)",
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
const insertBatch = db.transaction((items: BatchAccountEntry[]) => {
|
|
223
|
+
for (const item of items) {
|
|
224
|
+
const email = item.email.trim();
|
|
225
|
+
const password = item.password;
|
|
226
|
+
if (!email || !password) {
|
|
227
|
+
invalid.push(email || "(empty)");
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (existingEmails.has(email.toLowerCase())) {
|
|
232
|
+
skipped.push(email);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const newAccount: QwenAccount = {
|
|
237
|
+
id: crypto.randomUUID(),
|
|
238
|
+
email,
|
|
239
|
+
password,
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
insertStmt.run(newAccount.id, newAccount.email, encrypt(newAccount.password));
|
|
243
|
+
existingEmails.add(email.toLowerCase());
|
|
244
|
+
added.push(newAccount);
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
insertBatch(entries);
|
|
249
|
+
|
|
250
|
+
if (added.length > 0) {
|
|
251
|
+
invalidateAccountsCache();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { added, skipped, invalid };
|
|
255
|
+
}
|
|
126
256
|
export function addAccount(
|
|
127
257
|
email: string,
|
|
128
258
|
password: string,
|
package/src/core/config.ts
CHANGED
|
@@ -34,7 +34,18 @@ const envSchema = z
|
|
|
34
34
|
// temp chat (chat_mode:"local") for every request and sends the full
|
|
35
35
|
// history inline (OpenAI standard). Temp chats are ephemeral and never
|
|
36
36
|
// appear in the account's chat list (live-probed).
|
|
37
|
-
QWEN_CHAT_MODE: z
|
|
37
|
+
QWEN_CHAT_MODE: z
|
|
38
|
+
.enum([
|
|
39
|
+
"thread",
|
|
40
|
+
"temp",
|
|
41
|
+
"temp-thread",
|
|
42
|
+
"thread-temp",
|
|
43
|
+
"stateless",
|
|
44
|
+
"stateles",
|
|
45
|
+
"stateless-temp",
|
|
46
|
+
"stateles-temp",
|
|
47
|
+
])
|
|
48
|
+
.default("thread"),
|
|
38
49
|
PLAYWRIGHT_HEADLESS: z.string().default("true"),
|
|
39
50
|
PLAYWRIGHT_BROWSER: z
|
|
40
51
|
.enum(["chromium", "chrome", "edge"])
|
|
@@ -362,7 +373,7 @@ export const config = {
|
|
|
362
373
|
/** When true, all requests (personalization, models, media, chat) route exclusively through the browser page (no direct Node fetch). */
|
|
363
374
|
browserOnlyFetch: env.QWEN_BROWSER_ONLY_FETCH !== "false",
|
|
364
375
|
mapOpenAiModels: env.QWEN_MAP_OPENAI_MODELS !== "false",
|
|
365
|
-
chatMode: env.QWEN_CHAT_MODE,
|
|
376
|
+
chatMode: normalizeChatMode(env.QWEN_CHAT_MODE),
|
|
366
377
|
maxPromptBytes: Math.max(0, parseInt(env.QWEN_MAX_PROMPT_BYTES)),
|
|
367
378
|
maxPersonalizationBytes: Math.max(
|
|
368
379
|
0,
|
|
@@ -388,5 +399,85 @@ export const config = {
|
|
|
388
399
|
|
|
389
400
|
export type Config = typeof config;
|
|
390
401
|
|
|
391
|
-
/**
|
|
392
|
-
|
|
402
|
+
/**
|
|
403
|
+
* Conversation modes:
|
|
404
|
+
* - "thread": thread-native delta (~1KB), saved in Qwen account sidebar (default)
|
|
405
|
+
* - "thread-temp" (alias "temp-thread"): thread-native delta (~1KB), ephemeral local chat (not saved)
|
|
406
|
+
* - "stateless": full history re-sent every turn (OpenAI standard), saved in Qwen account sidebar
|
|
407
|
+
* - "stateless-temp" (alias "temp"): full history re-sent every turn (OpenAI standard), ephemeral local chat (not saved)
|
|
408
|
+
*/
|
|
409
|
+
export type ChatMode =
|
|
410
|
+
| "thread"
|
|
411
|
+
| "thread-temp"
|
|
412
|
+
| "temp-thread"
|
|
413
|
+
| "stateless"
|
|
414
|
+
| "stateles"
|
|
415
|
+
| "stateless-temp"
|
|
416
|
+
| "stateles-temp"
|
|
417
|
+
| "temp";
|
|
418
|
+
|
|
419
|
+
export function isStatelessChatMode(mode?: string | null): boolean {
|
|
420
|
+
if (!mode) return false;
|
|
421
|
+
const m = mode.trim().toLowerCase().replace(/_/g, "-");
|
|
422
|
+
return (
|
|
423
|
+
m === "stateless" ||
|
|
424
|
+
m === "stateles" ||
|
|
425
|
+
m === "stateless-temp" ||
|
|
426
|
+
m === "stateles-temp" ||
|
|
427
|
+
m === "temp"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export function isLocalChatMode(mode?: string | null): boolean {
|
|
432
|
+
if (!mode) return false;
|
|
433
|
+
const m = mode.trim().toLowerCase().replace(/_/g, "-");
|
|
434
|
+
return (
|
|
435
|
+
m === "stateless-temp" ||
|
|
436
|
+
m === "stateles-temp" ||
|
|
437
|
+
m === "temp" ||
|
|
438
|
+
m === "thread-temp" ||
|
|
439
|
+
m === "temp-thread"
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function normalizeChatMode(
|
|
444
|
+
mode?: string | null,
|
|
445
|
+
): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
|
|
446
|
+
if (!mode) return "thread";
|
|
447
|
+
const m = mode.trim().toLowerCase().replace(/_/g, "-");
|
|
448
|
+
if (m === "stateless-temp" || m === "stateles-temp" || m === "temp") {
|
|
449
|
+
return "stateless-temp";
|
|
450
|
+
}
|
|
451
|
+
if (m === "stateless" || m === "stateles") {
|
|
452
|
+
return "stateless";
|
|
453
|
+
}
|
|
454
|
+
if (m === "thread-temp" || m === "temp-thread") {
|
|
455
|
+
return "thread-temp";
|
|
456
|
+
}
|
|
457
|
+
return "thread";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export function setRuntimeChatMode(
|
|
461
|
+
mode?: string | null,
|
|
462
|
+
): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
|
|
463
|
+
const normalized = normalizeChatMode(mode);
|
|
464
|
+
(config.qwen as { chatMode: "thread" | "thread-temp" | "stateless" | "stateless-temp" }).chatMode = normalized;
|
|
465
|
+
return normalized;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export function getRuntimeChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
|
|
469
|
+
return config.qwen.chatMode as "thread" | "thread-temp" | "stateless" | "stateless-temp";
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
export function cycleNextChatMode(): "thread" | "thread-temp" | "stateless" | "stateless-temp" {
|
|
473
|
+
const current = getRuntimeChatMode();
|
|
474
|
+
const order: Array<"thread" | "thread-temp" | "stateless-temp" | "stateless"> = [
|
|
475
|
+
"thread",
|
|
476
|
+
"thread-temp",
|
|
477
|
+
"stateless-temp",
|
|
478
|
+
"stateless",
|
|
479
|
+
];
|
|
480
|
+
const idx = order.indexOf(current);
|
|
481
|
+
const next = order[(idx + 1) % order.length];
|
|
482
|
+
return setRuntimeChatMode(next);
|
|
483
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { config, type ChatMode } from "../../core/config.ts";
|
|
1
|
+
import { config, type ChatMode, isStatelessChatMode } from "../../core/config.ts";
|
|
2
2
|
import { ContextLengthExceededError, ValidationError } from "../../core/errors.ts";
|
|
3
3
|
import { getModelContextWindow } from "../../core/model-registry.ts";
|
|
4
4
|
import {
|
|
@@ -61,16 +61,16 @@ export async function buildFinalContext(
|
|
|
61
61
|
|
|
62
62
|
const modelContextWindow = getModelContextWindow(modelId);
|
|
63
63
|
const useThreadNative = true;
|
|
64
|
-
const
|
|
64
|
+
const isStateless = isStatelessChatMode(chatMode);
|
|
65
65
|
// A continuation is ANY evidence of a prior turn, not just a plain
|
|
66
66
|
// role:"assistant" message. Tool-loop clients (Zed/Cline) can send history
|
|
67
67
|
// with tool/function responses or assistant tool_calls but WITHOUT a plain
|
|
68
68
|
// assistant entry; misclassifying those as a new session forced the FULL
|
|
69
69
|
// history to be re-sent on every request (and every chat_in_progress retry)
|
|
70
70
|
// instead of the thread-native delta.
|
|
71
|
-
// In temp
|
|
72
|
-
// is always sent and no thread state is
|
|
73
|
-
const isNewSession =
|
|
71
|
+
// In stateless modes (stateless, stateless-temp, temp) EVERY request is a
|
|
72
|
+
// new chat, so the whole history is always sent and no thread state is consulted.
|
|
73
|
+
const isNewSession = isStateless
|
|
74
74
|
? true
|
|
75
75
|
: !messages.some(isContinuationMessage);
|
|
76
76
|
const completeInstructions = [systemPrompt.trim(), toolInstructions.trim()]
|
|
@@ -83,7 +83,7 @@ export async function buildFinalContext(
|
|
|
83
83
|
// OR: this is a continuation (has assistant messages in history)
|
|
84
84
|
// This prevents new IDE chats from accidentally reusing old Qwen chats
|
|
85
85
|
// while still allowing continuations without explicit session_id
|
|
86
|
-
const allowThreadReuse =
|
|
86
|
+
const allowThreadReuse = isStateless
|
|
87
87
|
? false
|
|
88
88
|
: useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
|
|
89
89
|
|
|
@@ -91,7 +91,7 @@ export async function buildFinalContext(
|
|
|
91
91
|
// an explicit conversation key. Otherwise, generate an ephemeral ID for
|
|
92
92
|
// logging/metrics only (not used for thread reuse). Temp mode never persists
|
|
93
93
|
// a thread, so it has no session id.
|
|
94
|
-
const sessionId =
|
|
94
|
+
const sessionId = isStateless
|
|
95
95
|
? null
|
|
96
96
|
: (conversationKey || useThreadNative)
|
|
97
97
|
? deriveSessionId(
|
|
@@ -110,7 +110,7 @@ export async function buildFinalContext(
|
|
|
110
110
|
// Thread-native: send full history when Qwen has no context yet, but preserve
|
|
111
111
|
// tool-result deltas because the upstream parent chain already owns the call.
|
|
112
112
|
// Temp mode: always send the FULL history (OpenAI standard).
|
|
113
|
-
const baseActivePrompt =
|
|
113
|
+
const baseActivePrompt = isStateless
|
|
114
114
|
? prompt
|
|
115
115
|
: (!existingThread && !hasTrailingToolResult ? prompt : currentPrompt) ||
|
|
116
116
|
prompt;
|
|
@@ -179,7 +179,7 @@ export async function buildFinalContext(
|
|
|
179
179
|
isNewSession,
|
|
180
180
|
useThreadNative,
|
|
181
181
|
// Thread state is only persisted in thread mode (temp chats are ephemeral).
|
|
182
|
-
updateLogicalThread:
|
|
182
|
+
updateLogicalThread: isStateless ? false : useThreadNative,
|
|
183
183
|
chatMode,
|
|
184
184
|
isThinkingModel,
|
|
185
185
|
estimatedTokens,
|
package/src/routes/chat/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
handleChatCompletionsError,
|
|
21
21
|
type AssistantCompleteEvent,
|
|
22
22
|
} from "./streaming.ts";
|
|
23
|
-
import { config, type ChatMode } from "../../core/config.ts";
|
|
23
|
+
import { config, type ChatMode, normalizeChatMode } from "../../core/config.ts";
|
|
24
24
|
import { logger } from "../../core/logger.ts";
|
|
25
25
|
import { metrics } from "../../core/metrics.ts";
|
|
26
26
|
import { getContextMeterHeaders, type ContextMeterMode } from "../../services/context-meter.ts";
|
|
@@ -50,11 +50,20 @@ function formatTimingHeader(timings: Record<string, number>): string {
|
|
|
50
50
|
* else silently uses the configured default.
|
|
51
51
|
*/
|
|
52
52
|
function resolveChatMode(headerValue: string | undefined): ChatMode {
|
|
53
|
-
if (headerValue
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
if (headerValue) {
|
|
54
|
+
const m = headerValue.trim().toLowerCase().replace(/_/g, "-");
|
|
55
|
+
if (
|
|
56
|
+
m === "thread" ||
|
|
57
|
+
m === "thread-temp" ||
|
|
58
|
+
m === "temp-thread" ||
|
|
59
|
+
m === "stateless" ||
|
|
60
|
+
m === "stateles" ||
|
|
61
|
+
m === "stateless-temp" ||
|
|
62
|
+
m === "stateles-temp" ||
|
|
63
|
+
m === "temp"
|
|
64
|
+
) {
|
|
65
|
+
return normalizeChatMode(m);
|
|
66
|
+
}
|
|
58
67
|
}
|
|
59
68
|
return config.qwen.chatMode;
|
|
60
69
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import crypto from "crypto";
|
|
2
2
|
import { getQwenHeaders, isAuthMockEnabled } from "./auth-playwright.ts";
|
|
3
|
-
import { config, type ChatMode } from "../core/config.ts";
|
|
3
|
+
import { config, type ChatMode, isLocalChatMode, isStatelessChatMode } from "../core/config.ts";
|
|
4
4
|
import { logger, isToolcallDebugEnabled } from "../core/logger.ts";
|
|
5
5
|
import {
|
|
6
6
|
computeQuotaCooldownMs,
|
|
@@ -83,9 +83,8 @@ export function buildChatNewBody(
|
|
|
83
83
|
project_id: "",
|
|
84
84
|
timestamp: Date.now(),
|
|
85
85
|
chat_type: "t2t",
|
|
86
|
-
// thread
|
|
87
|
-
chat_mode:
|
|
88
|
-
chatMode === "temp" || chatMode === "temp-thread" ? "local" : "normal",
|
|
86
|
+
// normal: thread & stateless (persisted in Qwen); local: thread-temp & stateless-temp (ephemeral)
|
|
87
|
+
chat_mode: isLocalChatMode(chatMode) ? "local" : "normal",
|
|
89
88
|
};
|
|
90
89
|
}
|
|
91
90
|
|
|
@@ -201,7 +200,7 @@ export async function acquireNewQwenChatSession(
|
|
|
201
200
|
accountId?: string,
|
|
202
201
|
chatMode: ChatMode = "thread",
|
|
203
202
|
): Promise<{ chatId: string; leasedFromPool: boolean }> {
|
|
204
|
-
if (isQwenChatPoolEnabled() && chatMode
|
|
203
|
+
if (isQwenChatPoolEnabled() && !isStatelessChatMode(chatMode)) {
|
|
205
204
|
const key = chatPoolKey(accountId, model);
|
|
206
205
|
const pooled = precreatedChatSessions.get(key);
|
|
207
206
|
const chatId = pooled?.shift();
|
package/src/services/qwen.ts
CHANGED
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "../core/errors.ts";
|
|
13
13
|
import { buildQwenRequestHeaders } from "./qwen-headers.ts";
|
|
14
14
|
import { qwenOrigin, qwenUrl } from "./qwen-url.ts";
|
|
15
|
-
import { config, type ChatMode } from "../core/config.ts";
|
|
15
|
+
import { config, type ChatMode, isLocalChatMode } from "../core/config.ts";
|
|
16
16
|
import { logger } from "../core/logger.ts";
|
|
17
17
|
import { estimateTokenCount } from "../utils/context-truncation.ts";
|
|
18
18
|
import type {
|
|
@@ -2757,10 +2757,7 @@ async function createQwenStreamInternal(
|
|
|
2757
2757
|
chatId: chatSessionId || null,
|
|
2758
2758
|
parentId: actualParentId ?? "",
|
|
2759
2759
|
chat_id: chatSessionId || null,
|
|
2760
|
-
chat_mode:
|
|
2761
|
-
options?.chatMode === "temp" || options?.chatMode === "temp-thread"
|
|
2762
|
-
? "local"
|
|
2763
|
-
: "normal",
|
|
2760
|
+
chat_mode: isLocalChatMode(options?.chatMode) ? "local" : "normal",
|
|
2764
2761
|
model: model,
|
|
2765
2762
|
parent_id: actualParentId,
|
|
2766
2763
|
messages: [
|