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.
Files changed (109) hide show
  1. package/LICENSE +14 -14
  2. package/README.md +906 -906
  3. package/bin/qwenproxy.js +5 -1
  4. package/package.json +77 -78
  5. package/src/api/error-classifier.ts +159 -159
  6. package/src/api/error-helpers.ts +118 -118
  7. package/src/api/models.ts +261 -261
  8. package/src/api/server.ts +860 -859
  9. package/src/cache/memory-cache.ts +385 -385
  10. package/src/clean-cache.ts +204 -204
  11. package/src/core/account-concurrency.ts +671 -671
  12. package/src/core/account-manager.ts +301 -297
  13. package/src/core/account-priority.ts +163 -163
  14. package/src/core/accounts.ts +186 -186
  15. package/src/core/config.ts +383 -383
  16. package/src/core/crypto-utils.ts +79 -79
  17. package/src/core/database.ts +276 -276
  18. package/src/core/errors.ts +118 -118
  19. package/src/core/logger.ts +269 -269
  20. package/src/core/memory-usage.ts +84 -84
  21. package/src/core/metrics.ts +291 -291
  22. package/src/core/model-alias.ts +77 -77
  23. package/src/core/model-registry.ts +544 -544
  24. package/src/core/mutex.ts +119 -119
  25. package/src/core/paths.ts +199 -199
  26. package/src/core/prompt-limits.ts +214 -214
  27. package/src/core/reasoning-effort.ts +102 -102
  28. package/src/core/stream-registry.ts +96 -96
  29. package/src/core/waf-isolation.ts +117 -117
  30. package/src/core/watchdog.ts +195 -195
  31. package/src/delete-chats.ts +23 -23
  32. package/src/index.ts +65 -64
  33. package/src/login.ts +147 -147
  34. package/src/reset-cooldowns.ts +11 -11
  35. package/src/routes/anthropic/index.ts +355 -355
  36. package/src/routes/anthropic/translate.ts +522 -522
  37. package/src/routes/anthropic/types.ts +154 -154
  38. package/src/routes/anthropic/validation.ts +144 -144
  39. package/src/routes/chat/account.ts +1817 -1817
  40. package/src/routes/chat/context.ts +241 -241
  41. package/src/routes/chat/errors.ts +85 -85
  42. package/src/routes/chat/helpers.ts +268 -268
  43. package/src/routes/chat/index.ts +618 -618
  44. package/src/routes/chat/media.ts +285 -285
  45. package/src/routes/chat/retry-policy.ts +754 -754
  46. package/src/routes/chat/stop.ts +98 -98
  47. package/src/routes/chat/streaming.ts +2710 -2710
  48. package/src/routes/chat/validation.ts +526 -526
  49. package/src/routes/chat.ts +2 -2
  50. package/src/routes/completions.ts +290 -290
  51. package/src/routes/images.ts +139 -139
  52. package/src/routes/responses/adapter.ts +503 -503
  53. package/src/routes/responses/index.ts +405 -405
  54. package/src/routes/responses/state.ts +230 -230
  55. package/src/routes/responses/streaming.ts +528 -528
  56. package/src/routes/responses/types.ts +285 -285
  57. package/src/routes/responses/validation.ts +202 -202
  58. package/src/routes/upload.ts +731 -731
  59. package/src/routes/videos.ts +214 -214
  60. package/src/services/auth-playwright.ts +173 -173
  61. package/src/services/captcha-coordinator.ts +161 -161
  62. package/src/services/captcha-solver.ts +553 -553
  63. package/src/services/chat-cleanup.ts +80 -80
  64. package/src/services/context-meter.ts +317 -317
  65. package/src/services/fingerprint.ts +242 -242
  66. package/src/services/human-behavior.ts +173 -173
  67. package/src/services/media-generation.ts +1748 -1748
  68. package/src/services/playwright.ts +2878 -2800
  69. package/src/services/qwen-chat-pool.ts +345 -345
  70. package/src/services/qwen-errors.ts +133 -133
  71. package/src/services/qwen-headers.ts +79 -79
  72. package/src/services/qwen-thread-state.ts +393 -393
  73. package/src/services/qwen-url.ts +19 -19
  74. package/src/services/qwen.ts +3126 -3126
  75. package/src/services/session-keeper.ts +88 -88
  76. package/src/services/token-estimation-metrics.ts +118 -118
  77. package/src/sync/claude-code.ts +75 -75
  78. package/src/sync/codex.ts +123 -123
  79. package/src/sync/index.ts +362 -362
  80. package/src/sync/omp.ts +105 -105
  81. package/src/sync/opencode.ts +214 -214
  82. package/src/sync/types.ts +53 -53
  83. package/src/sync/utils.ts +27 -27
  84. package/src/sync-clients.ts +189 -189
  85. package/src/tools/instructions.ts +137 -137
  86. package/src/tools/manifest.ts +81 -81
  87. package/src/tools/parser.ts +2989 -2989
  88. package/src/tools/toolcall-tags.ts +142 -142
  89. package/src/tui/app.ts +259 -264
  90. package/src/tui/index.ts +61 -61
  91. package/src/tui/markdown.ts +258 -258
  92. package/src/tui/proxy-client.ts +331 -326
  93. package/src/tui/screen.ts +294 -278
  94. package/src/tui/server-manager.ts +270 -270
  95. package/src/tui/theme.ts +432 -432
  96. package/src/tui/types.ts +33 -33
  97. package/src/tui/views/accounts-view.ts +656 -656
  98. package/src/tui/views/chat-view.ts +1018 -823
  99. package/src/tui/views/logs-view.ts +479 -413
  100. package/src/tui/views/status-view.ts +204 -204
  101. package/src/tui/views/storage-view.ts +304 -291
  102. package/src/tui/views/sync-view.ts +409 -409
  103. package/src/types/ali-oss.d.ts +32 -32
  104. package/src/update-cli.ts +121 -0
  105. package/src/utils/context-truncation.ts +84 -84
  106. package/src/utils/json.ts +380 -380
  107. package/src/utils/session-id.ts +37 -37
  108. package/src/utils/tool-call-guard.ts +84 -84
  109. 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.0",
4
- "description": "API compatível com OpenAI que conecta clientes ao Qwen via Playwright, com suporte a múltiplas contas, tool calling e sessões persistentes.",
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
- "postinstall": "patchright install chromium",
20
- "start": "tsx src/index.ts",
21
- "tui": "npx tsx src/tui/index.ts",
22
- "login": "npx tsx src/login.ts",
23
- "sync": "npx tsx src/sync-clients.ts",
24
- "clean": "npx tsx src/clean-cache.ts",
25
- "clean:all": "npx tsx src/clean-cache.ts --all",
26
- "reset": "npx tsx src/reset-cooldowns.ts",
27
- "purge": "npx tsx src/delete-chats.ts",
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/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: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",
31
- "test:live": "tsx --test src/tests/concurrentChat.test.ts src/tests/agenticStress.test.ts",
32
- "benchmark:proxy": "npx tsx src/benchmarks/proxy-baseline.ts",
33
- "typecheck": "tsc --noEmit"
34
- },
35
- "keywords": [
36
- "qwen",
37
- "proxy",
38
- "openai",
39
- "anthropic",
40
- "claude-code",
41
- "codex",
42
- "opencode",
43
- "tui",
44
- "patchright",
45
- "playwright",
46
- "llm"
47
- ],
48
- "homepage": "https://github.com/johngbl/qwenproxy#readme",
49
- "repository": {
50
- "type": "git",
51
- "url": "git+https://github.com/johngbl/qwenproxy.git"
52
- },
53
- "bugs": {
54
- "url": "https://github.com/johngbl/qwenproxy/issues"
55
- },
56
- "author": "johngbl <147551701+johngbl@users.noreply.github.com>",
57
- "license": "ISC",
58
- "dependencies": {
59
- "@hono/node-server": "^2.1.1",
60
- "ali-oss": "^6.23.0",
61
- "better-sqlite3": "^13.0.3",
62
- "dotenv": "^17.4.2",
63
- "hono": "^4.13.7",
64
- "patchright": "^1.62.3",
65
- "tsx": "^4.23.13",
66
- "uuid": "^14.0.2",
67
- "zod": "^4.5.4"
68
- },
69
- "devDependencies": {
70
- "@types/better-sqlite3": "^9.6.0",
71
- "@types/node": "^26.4.1",
72
- "typescript": "^7.0.2"
73
- },
74
- "type": "module",
75
- "engines": {
76
- "node": ">=22"
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
+ }