qwenproxy-cli 1.0.0 → 1.0.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/LICENSE +14 -14
- package/README.md +906 -906
- package/bin/qwenproxy.js +5 -1
- package/package.json +77 -78
- package/src/api/error-classifier.ts +159 -159
- package/src/api/error-helpers.ts +118 -118
- package/src/api/models.ts +261 -261
- package/src/api/server.ts +860 -859
- package/src/cache/memory-cache.ts +385 -385
- package/src/clean-cache.ts +204 -204
- package/src/core/account-concurrency.ts +671 -671
- package/src/core/account-manager.ts +301 -297
- package/src/core/account-priority.ts +163 -163
- package/src/core/accounts.ts +186 -186
- package/src/core/config.ts +383 -383
- package/src/core/crypto-utils.ts +79 -79
- package/src/core/database.ts +276 -276
- package/src/core/errors.ts +118 -118
- package/src/core/logger.ts +269 -269
- package/src/core/memory-usage.ts +84 -84
- package/src/core/metrics.ts +291 -291
- package/src/core/model-alias.ts +77 -77
- package/src/core/model-registry.ts +544 -544
- package/src/core/mutex.ts +119 -119
- package/src/core/paths.ts +199 -199
- package/src/core/prompt-limits.ts +214 -214
- package/src/core/reasoning-effort.ts +102 -102
- package/src/core/stream-registry.ts +96 -96
- package/src/core/waf-isolation.ts +117 -117
- package/src/core/watchdog.ts +195 -195
- package/src/delete-chats.ts +23 -23
- package/src/index.ts +65 -64
- package/src/login.ts +147 -147
- package/src/reset-cooldowns.ts +11 -11
- package/src/routes/anthropic/index.ts +355 -355
- package/src/routes/anthropic/translate.ts +522 -522
- package/src/routes/anthropic/types.ts +154 -154
- package/src/routes/anthropic/validation.ts +144 -144
- package/src/routes/chat/account.ts +1817 -1817
- package/src/routes/chat/context.ts +241 -241
- package/src/routes/chat/errors.ts +85 -85
- package/src/routes/chat/helpers.ts +268 -268
- package/src/routes/chat/index.ts +618 -618
- package/src/routes/chat/media.ts +285 -285
- package/src/routes/chat/retry-policy.ts +754 -754
- package/src/routes/chat/stop.ts +98 -98
- package/src/routes/chat/streaming.ts +2710 -2710
- package/src/routes/chat/validation.ts +526 -526
- package/src/routes/chat.ts +2 -2
- package/src/routes/completions.ts +290 -290
- package/src/routes/images.ts +139 -139
- package/src/routes/responses/adapter.ts +503 -503
- package/src/routes/responses/index.ts +405 -405
- package/src/routes/responses/state.ts +230 -230
- package/src/routes/responses/streaming.ts +528 -528
- package/src/routes/responses/types.ts +285 -285
- package/src/routes/responses/validation.ts +202 -202
- package/src/routes/upload.ts +731 -731
- package/src/routes/videos.ts +214 -214
- package/src/services/auth-playwright.ts +173 -173
- package/src/services/captcha-coordinator.ts +161 -161
- package/src/services/captcha-solver.ts +553 -553
- package/src/services/chat-cleanup.ts +80 -80
- package/src/services/context-meter.ts +317 -317
- package/src/services/fingerprint.ts +242 -242
- package/src/services/human-behavior.ts +173 -173
- package/src/services/media-generation.ts +1748 -1748
- package/src/services/playwright.ts +2878 -2800
- package/src/services/qwen-chat-pool.ts +345 -345
- package/src/services/qwen-errors.ts +133 -133
- package/src/services/qwen-headers.ts +79 -79
- package/src/services/qwen-thread-state.ts +393 -393
- package/src/services/qwen-url.ts +19 -19
- package/src/services/qwen.ts +3126 -3126
- package/src/services/session-keeper.ts +88 -88
- package/src/services/token-estimation-metrics.ts +118 -118
- package/src/sync/claude-code.ts +75 -75
- package/src/sync/codex.ts +123 -123
- package/src/sync/index.ts +362 -362
- package/src/sync/omp.ts +105 -105
- package/src/sync/opencode.ts +214 -214
- package/src/sync/types.ts +53 -53
- package/src/sync/utils.ts +27 -27
- package/src/sync-clients.ts +189 -189
- package/src/tools/instructions.ts +137 -137
- package/src/tools/manifest.ts +81 -81
- package/src/tools/parser.ts +2989 -2989
- package/src/tools/toolcall-tags.ts +142 -142
- package/src/tui/app.ts +259 -264
- package/src/tui/index.ts +61 -61
- package/src/tui/markdown.ts +258 -258
- package/src/tui/proxy-client.ts +331 -326
- package/src/tui/screen.ts +294 -278
- package/src/tui/server-manager.ts +270 -270
- package/src/tui/theme.ts +432 -432
- package/src/tui/types.ts +33 -33
- package/src/tui/views/accounts-view.ts +656 -656
- package/src/tui/views/chat-view.ts +1018 -823
- package/src/tui/views/logs-view.ts +479 -413
- package/src/tui/views/status-view.ts +204 -204
- package/src/tui/views/storage-view.ts +304 -291
- package/src/tui/views/sync-view.ts +409 -409
- package/src/types/ali-oss.d.ts +32 -32
- package/src/update-cli.ts +121 -0
- package/src/utils/context-truncation.ts +84 -84
- package/src/utils/json.ts +380 -380
- package/src/utils/session-id.ts +37 -37
- package/src/utils/tool-call-guard.ts +84 -84
- package/src/utils/types.ts +109 -109
package/bin/qwenproxy.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
* qwenproxy purge -> Deletes remote chats across configured accounts
|
|
14
14
|
* qwenproxy login -> Authenticates accounts via visible browser
|
|
15
15
|
*/
|
|
16
|
+
process.env.DOTENV_CONFIG_QUIET = "true";
|
|
16
17
|
|
|
17
18
|
import { spawn, spawnSync } from "node:child_process";
|
|
18
19
|
import path from "node:path";
|
|
@@ -51,6 +52,7 @@ Usage:
|
|
|
51
52
|
Commands:
|
|
52
53
|
(default) Launch interactive TUI management dashboard and proxy
|
|
53
54
|
start Run headless HTTP/SSE proxy server
|
|
55
|
+
update Check for updates and automatically update (npm, pnpm, bun)
|
|
54
56
|
login Authenticate new accounts via visible browser
|
|
55
57
|
sync Synchronize AI clients (Claude Code, Codex, OpenCode, OMP)
|
|
56
58
|
clean Prune transient profile caches
|
|
@@ -93,10 +95,12 @@ if (firstArg === "start" || rawArgs.includes("--server")) {
|
|
|
93
95
|
} else if (firstArg === "purge") {
|
|
94
96
|
scriptFile = "src/delete-chats.ts";
|
|
95
97
|
scriptArgs = rawArgs.slice(1);
|
|
98
|
+
} else if (firstArg === "update") {
|
|
99
|
+
scriptFile = "src/update-cli.ts";
|
|
100
|
+
scriptArgs = rawArgs.slice(1);
|
|
96
101
|
} else if (firstArg === "login") {
|
|
97
102
|
scriptFile = "src/login.ts";
|
|
98
103
|
scriptArgs = rawArgs.slice(1);
|
|
99
|
-
} else {
|
|
100
104
|
// Pass through remaining options to index.ts with default TUI flag
|
|
101
105
|
scriptFile = "src/index.ts";
|
|
102
106
|
scriptArgs = ["--tui", ...rawArgs];
|
package/package.json
CHANGED
|
@@ -1,78 +1,77 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "qwenproxy-cli",
|
|
3
|
-
"version": "1.0.
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "src/index.ts",
|
|
6
|
-
"bin": {
|
|
7
|
-
"qwenproxy": "bin/qwenproxy.js",
|
|
8
|
-
"qpx": "bin/qwenproxy.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"bin",
|
|
12
|
-
"src",
|
|
13
|
-
"!src/tests",
|
|
14
|
-
"!src/benchmarks",
|
|
15
|
-
"README.md",
|
|
16
|
-
"LICENSE"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"clean": "npx tsx src/clean-cache.ts",
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"test": "
|
|
29
|
-
"test:mock": "tsx --test --test-concurrency=
|
|
30
|
-
"test:
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
"
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
"
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
"@types/
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
"
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "qwenproxy-cli",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "High-performance OpenAI & Anthropic compatible API gateway for Qwen with multi-account rotation, interactive TUI, and resilient tool calling.",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"bin": {
|
|
7
|
+
"qwenproxy": "bin/qwenproxy.js",
|
|
8
|
+
"qpx": "bin/qwenproxy.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src",
|
|
13
|
+
"!src/tests",
|
|
14
|
+
"!src/benchmarks",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "tsx src/index.ts",
|
|
20
|
+
"tui": "npx tsx src/tui/index.ts",
|
|
21
|
+
"login": "npx tsx src/login.ts",
|
|
22
|
+
"sync": "npx tsx src/sync-clients.ts",
|
|
23
|
+
"clean": "npx tsx src/clean-cache.ts",
|
|
24
|
+
"clean:all": "npx tsx src/clean-cache.ts --all",
|
|
25
|
+
"reset": "npx tsx src/reset-cooldowns.ts",
|
|
26
|
+
"purge": "npx tsx src/delete-chats.ts",
|
|
27
|
+
"test": "npm run test:mock && npm run test:live",
|
|
28
|
+
"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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts",
|
|
29
|
+
"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/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/cli.test.ts src/tests/paths.test.ts src/tests/tool-manifest.test.ts src/tests/tui.test.ts",
|
|
30
|
+
"test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
|
|
31
|
+
"benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
|
|
32
|
+
"typecheck": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"qwen",
|
|
36
|
+
"proxy",
|
|
37
|
+
"openai",
|
|
38
|
+
"anthropic",
|
|
39
|
+
"claude-code",
|
|
40
|
+
"codex",
|
|
41
|
+
"opencode",
|
|
42
|
+
"tui",
|
|
43
|
+
"patchright",
|
|
44
|
+
"playwright",
|
|
45
|
+
"llm"
|
|
46
|
+
],
|
|
47
|
+
"homepage": "https://github.com/johngbl/qwenproxy#readme",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/johngbl/qwenproxy.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/johngbl/qwenproxy/issues"
|
|
54
|
+
},
|
|
55
|
+
"author": "johngbl <147551701+johngbl@users.noreply.github.com>",
|
|
56
|
+
"license": "ISC",
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@hono/node-server": "^2.1.1",
|
|
59
|
+
"ali-oss": "^6.23.0",
|
|
60
|
+
"better-sqlite3": "^13.0.3",
|
|
61
|
+
"dotenv": "^17.4.2",
|
|
62
|
+
"hono": "^4.13.7",
|
|
63
|
+
"patchright": "^1.62.3",
|
|
64
|
+
"tsx": "^4.23.13",
|
|
65
|
+
"uuid": "^14.0.2",
|
|
66
|
+
"zod": "^4.5.4"
|
|
67
|
+
},
|
|
68
|
+
"devDependencies": {
|
|
69
|
+
"@types/better-sqlite3": "^9.6.0",
|
|
70
|
+
"@types/node": "^26.4.1",
|
|
71
|
+
"typescript": "^7.0.2"
|
|
72
|
+
},
|
|
73
|
+
"type": "module",
|
|
74
|
+
"engines": {
|
|
75
|
+
"node": ">=22"
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -1,159 +1,159 @@
|
|
|
1
|
-
import { ZodError } from "zod";
|
|
2
|
-
import { logger } from "../core/logger.js";
|
|
3
|
-
import {
|
|
4
|
-
QwenProxyError,
|
|
5
|
-
InternalError,
|
|
6
|
-
ValidationError,
|
|
7
|
-
UpstreamRateLimit,
|
|
8
|
-
UpstreamError,
|
|
9
|
-
UpstreamTimeout,
|
|
10
|
-
ServiceUnavailable,
|
|
11
|
-
ClientAbortedError,
|
|
12
|
-
} from "../core/errors.js";
|
|
13
|
-
import {
|
|
14
|
-
QwenNetworkError,
|
|
15
|
-
QwenUpstreamUnavailableError,
|
|
16
|
-
RetryableQwenStreamError,
|
|
17
|
-
QwenUpstreamError,
|
|
18
|
-
QwenSessionExpiredError,
|
|
19
|
-
getQwenErrorCode,
|
|
20
|
-
} from "../services/qwen.js";
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Classifies unknown errors into standard QwenProxyError hierarchy.
|
|
24
|
-
* Preserves specific error metadata when possible.
|
|
25
|
-
*/
|
|
26
|
-
export function classifyError(err: unknown): QwenProxyError {
|
|
27
|
-
// These errors are retryable upstream failures, not rate-limit responses.
|
|
28
|
-
// RetryableQwenStreamError inherits UpstreamRateLimit for legacy behavior,
|
|
29
|
-
// so classify the concrete network/upstream types before that base class.
|
|
30
|
-
if (
|
|
31
|
-
err instanceof QwenNetworkError ||
|
|
32
|
-
err instanceof QwenUpstreamUnavailableError
|
|
33
|
-
) {
|
|
34
|
-
return new UpstreamError(err.message);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (err instanceof RetryableQwenStreamError) {
|
|
38
|
-
const upstreamCode = getQwenErrorCode(err)?.toLowerCase() || "";
|
|
39
|
-
const message = err.message.toLowerCase();
|
|
40
|
-
|
|
41
|
-
// Content moderation rejections are client-side content policy violations,
|
|
42
|
-
// not upstream failures. Return 400 so the client can adjust its input.
|
|
43
|
-
if (
|
|
44
|
-
upstreamCode === "data_inspection_failed" ||
|
|
45
|
-
message.includes("content moderation") ||
|
|
46
|
-
message.includes("data_inspection_failed")
|
|
47
|
-
) {
|
|
48
|
-
const moderationError = new ValidationError(
|
|
49
|
-
`Content rejected by Qwen safety filter: ${err.message.replace(/^Qwen content moderation: [^:]+: /, "")}`,
|
|
50
|
-
);
|
|
51
|
-
(moderationError as any).code = "content_policy_violation";
|
|
52
|
-
return moderationError;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
const isActualRateLimit =
|
|
56
|
-
upstreamCode === "quota_limit" ||
|
|
57
|
-
upstreamCode === "ratelimited" ||
|
|
58
|
-
upstreamCode === "rate_limit" ||
|
|
59
|
-
upstreamCode === "rate_limit_exceeded" ||
|
|
60
|
-
message.includes("quota") ||
|
|
61
|
-
message.includes("upper limit for today");
|
|
62
|
-
|
|
63
|
-
return isActualRateLimit
|
|
64
|
-
? new UpstreamRateLimit(err.message)
|
|
65
|
-
: new UpstreamError(err.message);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
if (err instanceof QwenUpstreamError) {
|
|
69
|
-
return err;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (err instanceof QwenSessionExpiredError) {
|
|
73
|
-
return err;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (err instanceof QwenProxyError) {
|
|
77
|
-
return err;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Client disconnected before the stream could be created. This is not a
|
|
81
|
-
// server fault: the request has no listener anymore. Classify it as a silent
|
|
82
|
-
// abort (499) so callers neither emit a 500 nor count it as an error.
|
|
83
|
-
if (err instanceof Error && err.message.includes("client aborted")) {
|
|
84
|
-
return new ClientAbortedError(err.message);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
// Capacity saturation on the bridge's own account pool: this is a "try again
|
|
88
|
-
// later" condition, not a server fault. A hard 500 is misleading for the
|
|
89
|
-
// client (and for the retry classifier it hides a recoverable slot wait).
|
|
90
|
-
if (
|
|
91
|
-
err instanceof Error &&
|
|
92
|
-
(err as Error & { code?: string }).code === "account_busy"
|
|
93
|
-
) {
|
|
94
|
-
return new UpstreamRateLimit(err.message);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Some call sites attach an explicit hint on a plain Error (e.g.
|
|
98
|
-
// acquireUpstreamStream sets upstreamStatus=429 when the whole pool is in
|
|
99
|
-
// cooldown). Respect it instead of falling through to a misleading 500.
|
|
100
|
-
const rawError = err as Error | null | undefined;
|
|
101
|
-
|
|
102
|
-
// A mutex acquire timeout (chat lock / personalization lock / playwright
|
|
103
|
-
// page lock) means the resource is BUSY, not broken — the holder is a
|
|
104
|
-
// legitimate long generation or a stuck-but-recoverable page op. Mapping it
|
|
105
|
-
// to 500 made every concurrent request on a long chat turn fail hard with
|
|
106
|
-
// internal_server_error (2026-08-22 production log). Surface as retryable
|
|
107
|
-
// 503 so the client (or the retry policy) can wait and re-request.
|
|
108
|
-
if (
|
|
109
|
-
rawError instanceof Error &&
|
|
110
|
-
rawError.message.startsWith("Mutex[") &&
|
|
111
|
-
rawError.message.includes("acquire timeout")
|
|
112
|
-
) {
|
|
113
|
-
return new ServiceUnavailable(
|
|
114
|
-
`Resource busy (${rawError.message.substring(0, 200)}). Retry shortly.`,
|
|
115
|
-
);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const statusHint = (err as Error & { upstreamStatus?: number })?.upstreamStatus;
|
|
119
|
-
if (typeof statusHint === "number") {
|
|
120
|
-
const message =
|
|
121
|
-
rawError instanceof Error
|
|
122
|
-
? rawError.message
|
|
123
|
-
: typeof err === "string"
|
|
124
|
-
? err
|
|
125
|
-
: "Unknown upstream error";
|
|
126
|
-
switch (statusHint) {
|
|
127
|
-
case 429:
|
|
128
|
-
return new UpstreamRateLimit(message);
|
|
129
|
-
case 502:
|
|
130
|
-
return new UpstreamError(message);
|
|
131
|
-
case 503:
|
|
132
|
-
return new ServiceUnavailable(message);
|
|
133
|
-
case 504:
|
|
134
|
-
return new UpstreamTimeout(message);
|
|
135
|
-
default:
|
|
136
|
-
break; // unknown hint: fall through to the normal mapping
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (err instanceof ZodError) {
|
|
141
|
-
return new ValidationError(err.message);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (
|
|
145
|
-
err instanceof Error &&
|
|
146
|
-
(err.message.includes("Target page, context or browser has been closed") ||
|
|
147
|
-
err.message.includes("browserType.launchPersistentContext"))
|
|
148
|
-
) {
|
|
149
|
-
return new ServiceUnavailable(
|
|
150
|
-
"Falha ao inicializar o navegador Playwright para a conta. O perfil pode estar travado por outro processo ou o navegador fechou inesperadamente. Execute 'npm run reset'.",
|
|
151
|
-
);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
logger.warn("Unclassified error mapped to InternalError", {
|
|
155
|
-
error: err instanceof Error ? err.message : String(err),
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
return new InternalError(err instanceof Error ? err.message : String(err));
|
|
159
|
-
}
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
import { logger } from "../core/logger.js";
|
|
3
|
+
import {
|
|
4
|
+
QwenProxyError,
|
|
5
|
+
InternalError,
|
|
6
|
+
ValidationError,
|
|
7
|
+
UpstreamRateLimit,
|
|
8
|
+
UpstreamError,
|
|
9
|
+
UpstreamTimeout,
|
|
10
|
+
ServiceUnavailable,
|
|
11
|
+
ClientAbortedError,
|
|
12
|
+
} from "../core/errors.js";
|
|
13
|
+
import {
|
|
14
|
+
QwenNetworkError,
|
|
15
|
+
QwenUpstreamUnavailableError,
|
|
16
|
+
RetryableQwenStreamError,
|
|
17
|
+
QwenUpstreamError,
|
|
18
|
+
QwenSessionExpiredError,
|
|
19
|
+
getQwenErrorCode,
|
|
20
|
+
} from "../services/qwen.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Classifies unknown errors into standard QwenProxyError hierarchy.
|
|
24
|
+
* Preserves specific error metadata when possible.
|
|
25
|
+
*/
|
|
26
|
+
export function classifyError(err: unknown): QwenProxyError {
|
|
27
|
+
// These errors are retryable upstream failures, not rate-limit responses.
|
|
28
|
+
// RetryableQwenStreamError inherits UpstreamRateLimit for legacy behavior,
|
|
29
|
+
// so classify the concrete network/upstream types before that base class.
|
|
30
|
+
if (
|
|
31
|
+
err instanceof QwenNetworkError ||
|
|
32
|
+
err instanceof QwenUpstreamUnavailableError
|
|
33
|
+
) {
|
|
34
|
+
return new UpstreamError(err.message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (err instanceof RetryableQwenStreamError) {
|
|
38
|
+
const upstreamCode = getQwenErrorCode(err)?.toLowerCase() || "";
|
|
39
|
+
const message = err.message.toLowerCase();
|
|
40
|
+
|
|
41
|
+
// Content moderation rejections are client-side content policy violations,
|
|
42
|
+
// not upstream failures. Return 400 so the client can adjust its input.
|
|
43
|
+
if (
|
|
44
|
+
upstreamCode === "data_inspection_failed" ||
|
|
45
|
+
message.includes("content moderation") ||
|
|
46
|
+
message.includes("data_inspection_failed")
|
|
47
|
+
) {
|
|
48
|
+
const moderationError = new ValidationError(
|
|
49
|
+
`Content rejected by Qwen safety filter: ${err.message.replace(/^Qwen content moderation: [^:]+: /, "")}`,
|
|
50
|
+
);
|
|
51
|
+
(moderationError as any).code = "content_policy_violation";
|
|
52
|
+
return moderationError;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isActualRateLimit =
|
|
56
|
+
upstreamCode === "quota_limit" ||
|
|
57
|
+
upstreamCode === "ratelimited" ||
|
|
58
|
+
upstreamCode === "rate_limit" ||
|
|
59
|
+
upstreamCode === "rate_limit_exceeded" ||
|
|
60
|
+
message.includes("quota") ||
|
|
61
|
+
message.includes("upper limit for today");
|
|
62
|
+
|
|
63
|
+
return isActualRateLimit
|
|
64
|
+
? new UpstreamRateLimit(err.message)
|
|
65
|
+
: new UpstreamError(err.message);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (err instanceof QwenUpstreamError) {
|
|
69
|
+
return err;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (err instanceof QwenSessionExpiredError) {
|
|
73
|
+
return err;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (err instanceof QwenProxyError) {
|
|
77
|
+
return err;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Client disconnected before the stream could be created. This is not a
|
|
81
|
+
// server fault: the request has no listener anymore. Classify it as a silent
|
|
82
|
+
// abort (499) so callers neither emit a 500 nor count it as an error.
|
|
83
|
+
if (err instanceof Error && err.message.includes("client aborted")) {
|
|
84
|
+
return new ClientAbortedError(err.message);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Capacity saturation on the bridge's own account pool: this is a "try again
|
|
88
|
+
// later" condition, not a server fault. A hard 500 is misleading for the
|
|
89
|
+
// client (and for the retry classifier it hides a recoverable slot wait).
|
|
90
|
+
if (
|
|
91
|
+
err instanceof Error &&
|
|
92
|
+
(err as Error & { code?: string }).code === "account_busy"
|
|
93
|
+
) {
|
|
94
|
+
return new UpstreamRateLimit(err.message);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Some call sites attach an explicit hint on a plain Error (e.g.
|
|
98
|
+
// acquireUpstreamStream sets upstreamStatus=429 when the whole pool is in
|
|
99
|
+
// cooldown). Respect it instead of falling through to a misleading 500.
|
|
100
|
+
const rawError = err as Error | null | undefined;
|
|
101
|
+
|
|
102
|
+
// A mutex acquire timeout (chat lock / personalization lock / playwright
|
|
103
|
+
// page lock) means the resource is BUSY, not broken — the holder is a
|
|
104
|
+
// legitimate long generation or a stuck-but-recoverable page op. Mapping it
|
|
105
|
+
// to 500 made every concurrent request on a long chat turn fail hard with
|
|
106
|
+
// internal_server_error (2026-08-22 production log). Surface as retryable
|
|
107
|
+
// 503 so the client (or the retry policy) can wait and re-request.
|
|
108
|
+
if (
|
|
109
|
+
rawError instanceof Error &&
|
|
110
|
+
rawError.message.startsWith("Mutex[") &&
|
|
111
|
+
rawError.message.includes("acquire timeout")
|
|
112
|
+
) {
|
|
113
|
+
return new ServiceUnavailable(
|
|
114
|
+
`Resource busy (${rawError.message.substring(0, 200)}). Retry shortly.`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const statusHint = (err as Error & { upstreamStatus?: number })?.upstreamStatus;
|
|
119
|
+
if (typeof statusHint === "number") {
|
|
120
|
+
const message =
|
|
121
|
+
rawError instanceof Error
|
|
122
|
+
? rawError.message
|
|
123
|
+
: typeof err === "string"
|
|
124
|
+
? err
|
|
125
|
+
: "Unknown upstream error";
|
|
126
|
+
switch (statusHint) {
|
|
127
|
+
case 429:
|
|
128
|
+
return new UpstreamRateLimit(message);
|
|
129
|
+
case 502:
|
|
130
|
+
return new UpstreamError(message);
|
|
131
|
+
case 503:
|
|
132
|
+
return new ServiceUnavailable(message);
|
|
133
|
+
case 504:
|
|
134
|
+
return new UpstreamTimeout(message);
|
|
135
|
+
default:
|
|
136
|
+
break; // unknown hint: fall through to the normal mapping
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (err instanceof ZodError) {
|
|
141
|
+
return new ValidationError(err.message);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
err instanceof Error &&
|
|
146
|
+
(err.message.includes("Target page, context or browser has been closed") ||
|
|
147
|
+
err.message.includes("browserType.launchPersistentContext"))
|
|
148
|
+
) {
|
|
149
|
+
return new ServiceUnavailable(
|
|
150
|
+
"Falha ao inicializar o navegador Playwright para a conta. O perfil pode estar travado por outro processo ou o navegador fechou inesperadamente. Execute 'npm run reset'.",
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
logger.warn("Unclassified error mapped to InternalError", {
|
|
155
|
+
error: err instanceof Error ? err.message : String(err),
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
return new InternalError(err instanceof Error ? err.message : String(err));
|
|
159
|
+
}
|