@jsonstudio/rcc 0.90.814 → 0.90.876

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 (221) hide show
  1. package/README.md +8 -0
  2. package/configsamples/provider-default/ali-coding-plan/config.v2.json +76 -0
  3. package/configsamples/provider-default/antigravity/config.v2.json +142 -0
  4. package/configsamples/provider-default/ark-coding-plan/config.v2.json +64 -0
  5. package/configsamples/provider-default/crs/config.v2.json +54 -0
  6. package/configsamples/provider-default/deepseek-web/config.v2.json +56 -0
  7. package/configsamples/provider-default/gemini/config.v2.json +43 -0
  8. package/configsamples/provider-default/gemini-cli/config.v2.json +45 -0
  9. package/configsamples/provider-default/gemini-native/config.v2.json +208 -0
  10. package/configsamples/provider-default/glm/config.v2.json +33 -0
  11. package/configsamples/provider-default/glm-anthropic/config.v2.json +29 -0
  12. package/configsamples/provider-default/kimi/config.v2.json +25 -0
  13. package/configsamples/provider-default/lmstudio/config.v2.json +79 -0
  14. package/configsamples/provider-default/lmstudio-proxy/config.v2.json +78 -0
  15. package/configsamples/provider-default/manifest.json +31 -0
  16. package/configsamples/provider-default/meituan/config.v2.json +20 -0
  17. package/configsamples/provider-default/mimo/config.v2.json +26 -0
  18. package/configsamples/provider-default/modelscope/config.v2.json +81 -0
  19. package/configsamples/provider-default/my-openai/config.v2.json +20 -0
  20. package/configsamples/provider-default/nvidia/config.v2.json +32 -0
  21. package/configsamples/provider-default/opencode-zen-free/config.v2.json +56 -0
  22. package/configsamples/provider-default/openrouter/config.v2.json +210 -0
  23. package/configsamples/provider-default/qwen/config.v2.json +38 -0
  24. package/configsamples/provider-default/qwenchat/config.v2.json +53 -0
  25. package/configsamples/provider-default/tab/config.v2.json +26 -0
  26. package/configsamples/provider-default/tabglm/config.v2.json +77 -0
  27. package/dist/build-info.js +2 -2
  28. package/dist/cli/commands/config.d.ts +5 -0
  29. package/dist/cli/commands/config.js +369 -1
  30. package/dist/cli/commands/config.js.map +1 -1
  31. package/dist/cli/commands/examples.js +3 -0
  32. package/dist/cli/commands/examples.js.map +1 -1
  33. package/dist/cli/commands/init.js +25 -1
  34. package/dist/cli/commands/init.js.map +1 -1
  35. package/dist/cli/commands/launcher-kernel.js +122 -46
  36. package/dist/cli/commands/launcher-kernel.js.map +1 -1
  37. package/dist/cli/commands/start.js +60 -3
  38. package/dist/cli/commands/start.js.map +1 -1
  39. package/dist/cli/config/bundled-provider-pack.d.ts +20 -0
  40. package/dist/cli/config/bundled-provider-pack.js +146 -0
  41. package/dist/cli/config/bundled-provider-pack.js.map +1 -0
  42. package/dist/cli/register/status-config-commands.d.ts +2 -0
  43. package/dist/cli/register/status-config-commands.js.map +1 -1
  44. package/dist/cli.js +81 -28
  45. package/dist/cli.js.map +1 -1
  46. package/dist/debug/snapshot-store.js +2 -1
  47. package/dist/debug/snapshot-store.js.map +1 -1
  48. package/dist/index.js +23 -14
  49. package/dist/index.js.map +1 -1
  50. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js +1 -1
  51. package/dist/manager/modules/quota/provider-quota-daemon.model-backoff.js.map +1 -1
  52. package/dist/manager/quota/provider-quota-center.js +1 -1
  53. package/dist/manager/quota/provider-quota-center.js.map +1 -1
  54. package/dist/manager/storage/file-store.js +10 -0
  55. package/dist/manager/storage/file-store.js.map +1 -1
  56. package/dist/modules/llmswitch/bridge/snapshot-recorder-runtime.js +18 -1
  57. package/dist/modules/llmswitch/bridge/snapshot-recorder-runtime.js.map +1 -1
  58. package/dist/modules/llmswitch/bridge/snapshot-recorder.js +132 -51
  59. package/dist/modules/llmswitch/bridge/snapshot-recorder.js.map +1 -1
  60. package/dist/provider-sdk/provider-runtime-inference.js +2 -2
  61. package/dist/provider-sdk/provider-runtime-inference.js.map +1 -1
  62. package/dist/providers/auth/deepseek-account-token-acquirer.js +32 -3
  63. package/dist/providers/auth/deepseek-account-token-acquirer.js.map +1 -1
  64. package/dist/providers/core/api/provider-types.d.ts +11 -0
  65. package/dist/providers/core/config/service-profiles.js +1 -1
  66. package/dist/providers/core/runtime/deepseek-http-provider.d.ts +2 -0
  67. package/dist/providers/core/runtime/deepseek-http-provider.js +31 -1
  68. package/dist/providers/core/runtime/deepseek-http-provider.js.map +1 -1
  69. package/dist/providers/core/runtime/qwenchat-http-provider-helpers.d.ts +3 -2
  70. package/dist/providers/core/runtime/qwenchat-http-provider-helpers.js +513 -96
  71. package/dist/providers/core/runtime/qwenchat-http-provider-helpers.js.map +1 -1
  72. package/dist/providers/core/runtime/standard-tool-text-harvest.d.ts +8 -0
  73. package/dist/providers/core/runtime/standard-tool-text-harvest.js +24 -0
  74. package/dist/providers/core/runtime/standard-tool-text-harvest.js.map +1 -0
  75. package/dist/providers/core/runtime/standard-tool-text-request-transform.d.ts +4 -1
  76. package/dist/providers/core/runtime/standard-tool-text-request-transform.js +129 -3
  77. package/dist/providers/core/runtime/standard-tool-text-request-transform.js.map +1 -1
  78. package/dist/providers/core/utils/snapshot-writer.js +5 -2
  79. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  80. package/dist/providers/profile/provider-profile-loader.js +52 -1
  81. package/dist/providers/profile/provider-profile-loader.js.map +1 -1
  82. package/dist/providers/profile/provider-profile.d.ts +3 -0
  83. package/dist/server/handlers/handler-response-utils.js +1 -0
  84. package/dist/server/handlers/handler-response-utils.js.map +1 -1
  85. package/dist/server/handlers/images-handler.d.ts +9 -0
  86. package/dist/server/handlers/images-handler.js +258 -0
  87. package/dist/server/handlers/images-handler.js.map +1 -0
  88. package/dist/server/handlers/types.d.ts +7 -0
  89. package/dist/server/runtime/http-server/antigravity-startup-tasks.d.ts +3 -0
  90. package/dist/server/runtime/http-server/antigravity-startup-tasks.js +16 -0
  91. package/dist/server/runtime/http-server/antigravity-startup-tasks.js.map +1 -1
  92. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js +3 -18
  93. package/dist/server/runtime/http-server/daemon-admin/auth-handler.js.map +1 -1
  94. package/dist/server/runtime/http-server/daemon-admin/providers-handler-utils.d.ts +7 -0
  95. package/dist/server/runtime/http-server/daemon-admin/providers-handler-utils.js +17 -0
  96. package/dist/server/runtime/http-server/daemon-admin/providers-handler-utils.js.map +1 -1
  97. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +50 -17
  98. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  99. package/dist/server/runtime/http-server/daemon-admin-routes.js +32 -2
  100. package/dist/server/runtime/http-server/daemon-admin-routes.js.map +1 -1
  101. package/dist/server/runtime/http-server/executor/provider-response-converter.js +42 -13
  102. package/dist/server/runtime/http-server/executor/provider-response-converter.js.map +1 -1
  103. package/dist/server/runtime/http-server/executor/provider-response-utils.js +41 -3
  104. package/dist/server/runtime/http-server/executor/provider-response-utils.js.map +1 -1
  105. package/dist/server/runtime/http-server/executor/usage-aggregator.js +7 -7
  106. package/dist/server/runtime/http-server/executor/usage-aggregator.js.map +1 -1
  107. package/dist/server/runtime/http-server/executor/usage-logger.d.ts +9 -0
  108. package/dist/server/runtime/http-server/executor/usage-logger.js +35 -2
  109. package/dist/server/runtime/http-server/executor/usage-logger.js.map +1 -1
  110. package/dist/server/runtime/http-server/executor-metadata.js +12 -4
  111. package/dist/server/runtime/http-server/executor-metadata.js.map +1 -1
  112. package/dist/server/runtime/http-server/executor-pipeline.js +24 -15
  113. package/dist/server/runtime/http-server/executor-pipeline.js.map +1 -1
  114. package/dist/server/runtime/http-server/executor-provider.d.ts +6 -1
  115. package/dist/server/runtime/http-server/executor-provider.js +137 -5
  116. package/dist/server/runtime/http-server/executor-provider.js.map +1 -1
  117. package/dist/server/runtime/http-server/http-server-bootstrap.js +6 -0
  118. package/dist/server/runtime/http-server/http-server-bootstrap.js.map +1 -1
  119. package/dist/server/runtime/http-server/http-server-lifecycle.js +23 -15
  120. package/dist/server/runtime/http-server/http-server-lifecycle.js.map +1 -1
  121. package/dist/server/runtime/http-server/http-server-runtime-providers.js +14 -4
  122. package/dist/server/runtime/http-server/http-server-runtime-providers.js.map +1 -1
  123. package/dist/server/runtime/http-server/http-server-runtime-setup.js +83 -1
  124. package/dist/server/runtime/http-server/http-server-runtime-setup.js.map +1 -1
  125. package/dist/server/runtime/http-server/hub-shadow-compare.js +2 -41
  126. package/dist/server/runtime/http-server/hub-shadow-compare.js.map +1 -1
  127. package/dist/server/runtime/http-server/provider-routing-scope.d.ts +9 -0
  128. package/dist/server/runtime/http-server/provider-routing-scope.js +20 -0
  129. package/dist/server/runtime/http-server/provider-routing-scope.js.map +1 -0
  130. package/dist/server/runtime/http-server/provider-traffic-governor.d.ts +67 -0
  131. package/dist/server/runtime/http-server/provider-traffic-governor.js +467 -0
  132. package/dist/server/runtime/http-server/provider-traffic-governor.js.map +1 -0
  133. package/dist/server/runtime/http-server/request-executor.d.ts +8 -0
  134. package/dist/server/runtime/http-server/request-executor.js +446 -21
  135. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  136. package/dist/server/runtime/http-server/routes.js +13 -0
  137. package/dist/server/runtime/http-server/routes.js.map +1 -1
  138. package/dist/server/runtime/http-server/session-client-registry.js +30 -4
  139. package/dist/server/runtime/http-server/session-client-registry.js.map +1 -1
  140. package/dist/server/runtime/http-server/session-client-route-utils.d.ts +7 -0
  141. package/dist/server/runtime/http-server/session-client-route-utils.js +38 -0
  142. package/dist/server/runtime/http-server/session-client-route-utils.js.map +1 -1
  143. package/dist/server/runtime/http-server/session-client-routes.js +12 -2
  144. package/dist/server/runtime/http-server/session-client-routes.js.map +1 -1
  145. package/dist/server/utils/request-id-manager.js +42 -5
  146. package/dist/server/utils/request-id-manager.js.map +1 -1
  147. package/dist/server/utils/stage-logger.d.ts +1 -0
  148. package/dist/server/utils/stage-logger.js +27 -0
  149. package/dist/server/utils/stage-logger.js.map +1 -1
  150. package/dist/utils/errorsamples.js +3 -1
  151. package/dist/utils/errorsamples.js.map +1 -1
  152. package/dist/utils/sensitive-redaction.d.ts +1 -0
  153. package/dist/utils/sensitive-redaction.js +122 -0
  154. package/dist/utils/sensitive-redaction.js.map +1 -0
  155. package/dist/utils/snapshot-writer.js +162 -2
  156. package/dist/utils/snapshot-writer.js.map +1 -1
  157. package/docs/INSTALLATION_AND_QUICKSTART.md +14 -1
  158. package/docs/PORTS.md +12 -0
  159. package/docs/lmstudio-tool-calling.md +25 -0
  160. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/qwenchat-web-request.d.ts +3 -0
  161. package/node_modules/@jsonstudio/llms/dist/conversion/compat/actions/qwenchat-web-request.js +62 -0
  162. package/node_modules/@jsonstudio/llms/dist/conversion/compat/profiles/chat-qwenchat-web.json +47 -0
  163. package/node_modules/@jsonstudio/llms/dist/conversion/hub/node-support.js +5 -2
  164. package/node_modules/@jsonstudio/llms/dist/conversion/hub/operation-table/operation-table-runner.js +68 -7
  165. package/node_modules/@jsonstudio/llms/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper-from-chat.js +138 -3
  166. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-chat-process-request-utils.js +24 -0
  167. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-execute-chat-process-entry.js +7 -1
  168. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-execute-request-stage.js +7 -0
  169. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-heavy-input-fastpath.d.ts +24 -0
  170. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-heavy-input-fastpath.js +203 -0
  171. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-pipeline-route-and-outbound.js +17 -12
  172. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-stage-timing.d.ts +11 -0
  173. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/hub-stage-timing.js +82 -1
  174. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +47 -14
  175. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +43 -0
  176. package/node_modules/@jsonstudio/llms/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/client-remap-protocol-switch.js +222 -19
  177. package/node_modules/@jsonstudio/llms/dist/conversion/hub/policy/policy-engine.js +2 -2
  178. package/node_modules/@jsonstudio/llms/dist/conversion/hub/process/chat-process-pending-tool-sync.js +24 -7
  179. package/node_modules/@jsonstudio/llms/dist/conversion/hub/response/provider-response.js +90 -1
  180. package/node_modules/@jsonstudio/llms/dist/conversion/hub/snapshot-recorder.d.ts +1 -0
  181. package/node_modules/@jsonstudio/llms/dist/conversion/hub/snapshot-recorder.js +252 -1
  182. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge/utils.js +5 -3
  183. package/node_modules/@jsonstudio/llms/dist/conversion/responses/responses-openai-bridge.js +44 -4
  184. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils-openai-request.d.ts +3 -1
  185. package/node_modules/@jsonstudio/llms/dist/conversion/shared/anthropic-message-utils-openai-request.js +20 -23
  186. package/node_modules/@jsonstudio/llms/dist/conversion/shared/tool-governor.js +68 -30
  187. package/node_modules/@jsonstudio/llms/dist/conversion/snapshot-utils.js +194 -10
  188. package/node_modules/@jsonstudio/llms/dist/native/router_hotpath_napi.node +0 -0
  189. package/node_modules/@jsonstudio/llms/dist/quota/quota-state.js +2 -2
  190. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine/routing-state/store.js +35 -2
  191. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/engine.js +9 -9
  192. package/node_modules/@jsonstudio/llms/dist/router/virtual-router/sticky-session-store.js +104 -18
  193. package/node_modules/@jsonstudio/llms/dist/servertool/engine.js +79 -32
  194. package/node_modules/@jsonstudio/llms/dist/servertool/handlers/vision.js +49 -0
  195. package/node_modules/@jsonstudio/llms/dist/servertool/pending-session.js +48 -2
  196. package/node_modules/@jsonstudio/llms/dist/servertool/server-side-tools.js +14 -1
  197. package/node_modules/@jsonstudio/llms/dist/servertool/types.d.ts +1 -0
  198. package/node_modules/@jsonstudio/llms/package.json +1 -1
  199. package/node_modules/ajv/dist/compile/jtd/serialize.js +9 -2
  200. package/node_modules/ajv/dist/compile/jtd/serialize.js.map +1 -1
  201. package/node_modules/ajv/dist/core.d.ts +1 -0
  202. package/node_modules/ajv/dist/core.js.map +1 -1
  203. package/node_modules/ajv/dist/vocabularies/validation/pattern.js +13 -4
  204. package/node_modules/ajv/dist/vocabularies/validation/pattern.js.map +1 -1
  205. package/node_modules/ajv/lib/compile/jtd/serialize.ts +13 -2
  206. package/node_modules/ajv/lib/core.ts +1 -0
  207. package/node_modules/ajv/lib/vocabularies/validation/pattern.ts +15 -4
  208. package/node_modules/ajv/package.json +2 -1
  209. package/package.json +15 -10
  210. package/scripts/ci/repo-sanity.mjs +23 -2
  211. package/scripts/ci/secrets-check.mjs +48 -0
  212. package/scripts/ci/silent-failure-audit.mjs +192 -0
  213. package/scripts/mock-provider/run-regressions.mjs +1 -0
  214. package/scripts/monitor/memory-guard.mjs +207 -0
  215. package/scripts/pack-mode.mjs +32 -36
  216. package/scripts/publish-rcc.mjs +38 -60
  217. package/scripts/tests/apply-patch-loop.mjs +1 -0
  218. package/scripts/tests/blackbox-rcc-vs-routecodex-antigravity.mjs +2 -0
  219. package/scripts/tools-dev/responses-debug-client/src/index.ts +8 -3
  220. package/scripts/verify-e2e-toolcall.mjs +1 -0
  221. package/scripts/verify-install-e2e.mjs +2 -1
@@ -1,7 +1,8 @@
1
1
  import { createHash, createHmac, randomBytes, randomUUID } from 'node:crypto';
2
+ import { isIP } from 'node:net';
2
3
  import { PassThrough } from 'node:stream';
3
- import { processChatResponseTools } from '@jsonstudio/llms/conversion';
4
4
  import { applyStandardToolTextRequestTransform } from './standard-tool-text-request-transform.js';
5
+ import { applyStandardToolTextHarvestToChatPayload } from './standard-tool-text-harvest.js';
5
6
  export const DEFAULT_QWENCHAT_BASE_URL = 'https://chat.qwen.ai';
6
7
  export const DEFAULT_QWENCHAT_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36';
7
8
  export const DEFAULT_QWENCHAT_ACCEPT_LANGUAGE = 'zh-CN,zh;q=0.9,en;q=0.8';
@@ -9,6 +10,8 @@ export const DEFAULT_QWENCHAT_COMPLETION_ENDPOINT = '/api/v2/chat/completions';
9
10
  export const DEFAULT_QWENCHAT_CHAT_CREATE_ENDPOINT = '/api/v2/chats/new';
10
11
  const BAXIA_VERSION = '2.5.36';
11
12
  const BAXIA_CACHE_TTL_MS = 4 * 60 * 1000;
13
+ const DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS = 15_000;
14
+ const DEFAULT_ATTACHMENT_MAX_BYTES = 20 * 1024 * 1024;
12
15
  function isRecord(value) {
13
16
  return value !== null && typeof value === 'object' && !Array.isArray(value);
14
17
  }
@@ -26,6 +29,214 @@ function normalizeInputString(value) {
26
29
  }
27
30
  return trimmed;
28
31
  }
32
+ function clampInteger(value, fallback, min, max) {
33
+ const parsed = typeof value === 'number' && Number.isFinite(value)
34
+ ? Math.trunc(value)
35
+ : Number.parseInt(normalizeInputString(value), 10);
36
+ if (!Number.isFinite(parsed)) {
37
+ return fallback;
38
+ }
39
+ return Math.min(max, Math.max(min, parsed));
40
+ }
41
+ function readBooleanEnv(keys, fallback) {
42
+ for (const key of keys) {
43
+ const raw = normalizeInputString(process.env[key]);
44
+ if (!raw) {
45
+ continue;
46
+ }
47
+ const normalized = raw.trim().toLowerCase();
48
+ if (['1', 'true', 'yes', 'on'].includes(normalized)) {
49
+ return true;
50
+ }
51
+ if (['0', 'false', 'no', 'off'].includes(normalized)) {
52
+ return false;
53
+ }
54
+ }
55
+ return fallback;
56
+ }
57
+ function parseImageSizeToQwenRatio(size) {
58
+ const text = normalizeInputString(size);
59
+ if (!text) {
60
+ return '1:1';
61
+ }
62
+ const ratioMatch = text.match(/^(\d{1,2}):(\d{1,2})$/);
63
+ if (ratioMatch) {
64
+ const w = Number.parseInt(ratioMatch[1], 10);
65
+ const h = Number.parseInt(ratioMatch[2], 10);
66
+ if (Number.isFinite(w) && Number.isFinite(h) && w > 0 && h > 0) {
67
+ const ratio = `${w}:${h}`;
68
+ if (['1:1', '16:9', '9:16', '4:3', '3:4'].includes(ratio)) {
69
+ return ratio;
70
+ }
71
+ }
72
+ }
73
+ const sizeMatch = text.toLowerCase().match(/^(\d{2,5})\s*x\s*(\d{2,5})$/);
74
+ if (!sizeMatch) {
75
+ return '1:1';
76
+ }
77
+ const width = Number.parseInt(sizeMatch[1], 10);
78
+ const height = Number.parseInt(sizeMatch[2], 10);
79
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
80
+ return '1:1';
81
+ }
82
+ const ratio = width / height;
83
+ const candidates = [
84
+ { key: '1:1', ratio: 1 },
85
+ { key: '16:9', ratio: 16 / 9 },
86
+ { key: '9:16', ratio: 9 / 16 },
87
+ { key: '4:3', ratio: 4 / 3 },
88
+ { key: '3:4', ratio: 3 / 4 }
89
+ ];
90
+ let best = candidates[0];
91
+ let bestDiff = Number.POSITIVE_INFINITY;
92
+ for (const candidate of candidates) {
93
+ const diff = Math.abs(ratio - candidate.ratio);
94
+ if (diff < bestDiff) {
95
+ best = candidate;
96
+ bestDiff = diff;
97
+ }
98
+ }
99
+ return best.key;
100
+ }
101
+ function parseQwenImageGenerationOptions(metadata) {
102
+ const node = metadata && isRecord(metadata.qwenImageGeneration)
103
+ ? metadata.qwenImageGeneration
104
+ : undefined;
105
+ const enabled = metadata?.qwenImageGeneration === true
106
+ || metadata?.imageGeneration === true
107
+ || normalizeInputString(metadata?.generationMode).toLowerCase() === 'image'
108
+ || Boolean(node);
109
+ const count = clampInteger(node?.n ?? metadata?.n, 1, 1, 10);
110
+ const sizeRatio = parseImageSizeToQwenRatio(node?.size ?? metadata?.size);
111
+ const responseFormatRaw = normalizeInputString(node?.responseFormat ?? node?.response_format ?? metadata?.response_format).toLowerCase();
112
+ const responseFormat = responseFormatRaw === 'b64_json' ? 'b64_json' : 'url';
113
+ return {
114
+ enabled,
115
+ count,
116
+ sizeRatio,
117
+ responseFormat
118
+ };
119
+ }
120
+ function readPositiveIntegerEnv(keys, fallback, min = 1, max = Number.MAX_SAFE_INTEGER) {
121
+ for (const key of keys) {
122
+ const raw = normalizeInputString(process.env[key]);
123
+ if (!raw) {
124
+ continue;
125
+ }
126
+ const parsed = Number.parseInt(raw, 10);
127
+ if (!Number.isFinite(parsed)) {
128
+ continue;
129
+ }
130
+ const normalized = Math.floor(parsed);
131
+ if (normalized < min || normalized > max) {
132
+ continue;
133
+ }
134
+ return normalized;
135
+ }
136
+ return fallback;
137
+ }
138
+ function isPrivateIpv4(host) {
139
+ const segments = host.split('.');
140
+ if (segments.length !== 4) {
141
+ return false;
142
+ }
143
+ const numbers = segments.map((segment) => Number.parseInt(segment, 10));
144
+ if (numbers.some((value) => !Number.isFinite(value) || value < 0 || value > 255)) {
145
+ return false;
146
+ }
147
+ const [a, b] = numbers;
148
+ if (a === 10 || a === 127 || a === 0) {
149
+ return true;
150
+ }
151
+ if (a === 169 && b === 254) {
152
+ return true;
153
+ }
154
+ if (a === 192 && b === 168) {
155
+ return true;
156
+ }
157
+ if (a === 172 && b >= 16 && b <= 31) {
158
+ return true;
159
+ }
160
+ return false;
161
+ }
162
+ function isPrivateIpv6(host) {
163
+ const normalized = host.toLowerCase();
164
+ return normalized === '::1'
165
+ || normalized.startsWith('fc')
166
+ || normalized.startsWith('fd')
167
+ || normalized.startsWith('fe80:');
168
+ }
169
+ function validateAttachmentSourceUrl(input) {
170
+ let parsed;
171
+ try {
172
+ parsed = new URL(input);
173
+ }
174
+ catch {
175
+ throw new Error('Invalid attachment URL');
176
+ }
177
+ const protocol = parsed.protocol.toLowerCase();
178
+ if (protocol !== 'https:' && protocol !== 'http:') {
179
+ throw new Error(`Unsupported attachment URL protocol: ${protocol}`);
180
+ }
181
+ if (parsed.username || parsed.password) {
182
+ throw new Error('Attachment URL must not include username/password');
183
+ }
184
+ const hostname = parsed.hostname.trim().toLowerCase();
185
+ if (!hostname) {
186
+ throw new Error('Attachment URL hostname is required');
187
+ }
188
+ if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname.endsWith('.local')) {
189
+ throw new Error('Attachment URL localhost/local domains are not allowed');
190
+ }
191
+ const ipType = isIP(hostname);
192
+ if (ipType === 4 && isPrivateIpv4(hostname)) {
193
+ throw new Error('Attachment URL private IPv4 is not allowed');
194
+ }
195
+ if (ipType === 6 && isPrivateIpv6(hostname)) {
196
+ throw new Error('Attachment URL private IPv6 is not allowed');
197
+ }
198
+ return parsed;
199
+ }
200
+ async function readResponseBytesWithLimit(response, maxBytes) {
201
+ const contentLengthText = normalizeInputString(response.headers.get('content-length'));
202
+ if (contentLengthText) {
203
+ const contentLength = Number.parseInt(contentLengthText, 10);
204
+ if (Number.isFinite(contentLength) && contentLength > maxBytes) {
205
+ throw new Error(`Attachment exceeds max size (${contentLength} > ${maxBytes})`);
206
+ }
207
+ }
208
+ const reader = response.body?.getReader();
209
+ if (!reader) {
210
+ return new Uint8Array(await response.arrayBuffer());
211
+ }
212
+ const chunks = [];
213
+ let total = 0;
214
+ while (true) {
215
+ const step = await reader.read();
216
+ if (step.done) {
217
+ break;
218
+ }
219
+ const chunk = step.value;
220
+ total += chunk.byteLength;
221
+ if (total > maxBytes) {
222
+ try {
223
+ await reader.cancel('attachment_too_large');
224
+ }
225
+ catch {
226
+ // ignore
227
+ }
228
+ throw new Error(`Attachment exceeds max size (${total} > ${maxBytes})`);
229
+ }
230
+ chunks.push(chunk);
231
+ }
232
+ const merged = new Uint8Array(total);
233
+ let offset = 0;
234
+ for (const chunk of chunks) {
235
+ merged.set(chunk, offset);
236
+ offset += chunk.byteLength;
237
+ }
238
+ return merged;
239
+ }
29
240
  function randomString(length) {
30
241
  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
31
242
  const bytes = randomBytes(length);
@@ -340,11 +551,19 @@ async function loadAttachmentBytes(attachment) {
340
551
  };
341
552
  }
342
553
  if (/^https?:\/\//i.test(attachment.source)) {
343
- const resp = await fetch(attachment.source);
554
+ const parsedUrl = validateAttachmentSourceUrl(attachment.source);
555
+ const timeoutMs = readPositiveIntegerEnv(['ROUTECODEX_QWENCHAT_ATTACHMENT_FETCH_TIMEOUT_MS', 'RCC_QWENCHAT_ATTACHMENT_FETCH_TIMEOUT_MS'], DEFAULT_ATTACHMENT_FETCH_TIMEOUT_MS, 1_000, 300_000);
556
+ const maxBytes = readPositiveIntegerEnv(['ROUTECODEX_QWENCHAT_ATTACHMENT_MAX_BYTES', 'RCC_QWENCHAT_ATTACHMENT_MAX_BYTES'], DEFAULT_ATTACHMENT_MAX_BYTES, 1024);
557
+ const controller = new AbortController();
558
+ const timer = setTimeout(() => controller.abort('attachment_fetch_timeout'), timeoutMs);
559
+ if (typeof timer.unref === 'function') {
560
+ timer.unref();
561
+ }
562
+ const resp = await fetch(parsedUrl, { signal: controller.signal }).finally(() => clearTimeout(timer));
344
563
  if (!resp.ok) {
345
564
  throw new Error(`Failed to fetch attachment URL: HTTP ${resp.status}`);
346
565
  }
347
- const bytes = new Uint8Array(await resp.arrayBuffer());
566
+ const bytes = await readResponseBytesWithLimit(resp, maxBytes);
348
567
  const mimeType = normalizeMimeType(attachment.mimeType || resp.headers.get('content-type'));
349
568
  return {
350
569
  bytes,
@@ -363,23 +582,45 @@ async function loadAttachmentBytes(attachment) {
363
582
  explicitType: attachment.explicitType
364
583
  };
365
584
  }
366
- function qwenCommonHeaders(baxiaTokens, authHeaders) {
585
+ function qwenCommonHeaders(baxiaTokens, authHeaders, options) {
586
+ const forwardAuthHeaders = readBooleanEnv(['ROUTECODEX_QWENCHAT_FORWARD_AUTH_HEADERS', 'RCC_QWENCHAT_FORWARD_AUTH_HEADERS'], false);
587
+ const normalizedBase = normalizeInputString(options?.baseUrl) || DEFAULT_QWENCHAT_BASE_URL;
588
+ const base = normalizedBase.replace(/\/$/, '');
589
+ const referer = options?.refererMode === 'guest' ? `${base}/c/guest` : `${base}/`;
590
+ const accept = options?.acceptMode === 'json' ? 'application/json' : 'application/json, text/plain, */*';
367
591
  return {
368
- 'Accept': 'application/json, text/plain, */*',
592
+ 'Accept': accept,
369
593
  'Content-Type': 'application/json',
370
594
  'bx-ua': baxiaTokens.bxUa,
371
595
  'bx-umidtoken': baxiaTokens.bxUmidToken,
372
596
  'bx-v': baxiaTokens.bxV,
373
597
  'source': 'web',
374
598
  'timezone': new Date().toUTCString(),
375
- 'Referer': `${DEFAULT_QWENCHAT_BASE_URL}/`,
599
+ 'Referer': referer,
376
600
  'User-Agent': DEFAULT_QWENCHAT_USER_AGENT,
377
601
  'Accept-Language': DEFAULT_QWENCHAT_ACCEPT_LANGUAGE,
378
602
  'x-request-id': randomUUID(),
379
- ...(authHeaders || {})
603
+ ...(forwardAuthHeaders ? authHeaders || {} : {})
380
604
  };
381
605
  }
382
606
  function extractQwenErrorMessage(payload) {
607
+ const readNestedMessage = (value) => {
608
+ if (typeof value === 'string') {
609
+ return normalizeInputString(value);
610
+ }
611
+ if (!isRecord(value)) {
612
+ return '';
613
+ }
614
+ const nested = [
615
+ normalizeInputString(value.message),
616
+ normalizeInputString(value.msg),
617
+ normalizeInputString(value.details),
618
+ normalizeInputString(value.template),
619
+ normalizeInputString(value.code),
620
+ isRecord(value.error) ? normalizeInputString(value.error.message) : ''
621
+ ].filter(Boolean);
622
+ return nested[0] || '';
623
+ };
383
624
  const readNode = (node) => {
384
625
  if (!node)
385
626
  return '';
@@ -387,7 +628,10 @@ function extractQwenErrorMessage(payload) {
387
628
  normalizeInputString(node.msg),
388
629
  normalizeInputString(node.message),
389
630
  normalizeInputString(node.error),
390
- normalizeInputString(node.err_msg)
631
+ normalizeInputString(node.err_msg),
632
+ normalizeInputString(node.details),
633
+ normalizeInputString(node.template),
634
+ normalizeInputString(node.code)
391
635
  ].filter(Boolean);
392
636
  if (direct.length > 0) {
393
637
  return direct[0];
@@ -402,10 +646,17 @@ function extractQwenErrorMessage(payload) {
402
646
  return nested[0];
403
647
  }
404
648
  }
649
+ if (isRecord(node.details)) {
650
+ const nestedDetails = readNestedMessage(node.details);
651
+ if (nestedDetails) {
652
+ return nestedDetails;
653
+ }
654
+ }
405
655
  return '';
406
656
  };
407
657
  const dataNode = isRecord(payload.data) ? payload.data : undefined;
408
- return readNode(payload) || readNode(dataNode) || '';
658
+ const detailsNode = isRecord(dataNode?.details) ? dataNode.details : undefined;
659
+ return readNode(payload) || readNode(dataNode) || readNode(detailsNode) || '';
409
660
  }
410
661
  function extractQwenUploadTokenData(payload) {
411
662
  const tryReadNode = (node) => {
@@ -462,7 +713,7 @@ async function requestUploadToken(baseUrl, file, baxiaTokens, authHeaders) {
462
713
  const filetype = inferFileCategory(file.mimeType, file.explicitType);
463
714
  const resp = await fetch(joinUrl(baseUrl, '/api/v2/files/getstsToken'), {
464
715
  method: 'POST',
465
- headers: qwenCommonHeaders(baxiaTokens, authHeaders),
716
+ headers: qwenCommonHeaders(baxiaTokens, authHeaders, { baseUrl, refererMode: 'web', acceptMode: 'default' }),
466
717
  body: JSON.stringify({
467
718
  filename: file.filename,
468
719
  filesize: file.bytes.length,
@@ -653,7 +904,7 @@ async function ensureUploadStatusForNonVideo(baseUrl, filetype, baxiaTokens, aut
653
904
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
654
905
  const resp = await fetch(joinUrl(baseUrl, '/api/v2/users/status'), {
655
906
  method: 'POST',
656
- headers: qwenCommonHeaders(baxiaTokens, authHeaders),
907
+ headers: qwenCommonHeaders(baxiaTokens, authHeaders, { baseUrl, refererMode: 'web', acceptMode: 'default' }),
657
908
  body: JSON.stringify({
658
909
  typarms: {
659
910
  typarm1: 'web',
@@ -689,7 +940,7 @@ async function parseDocumentIfNeeded(baseUrl, qwenFilePayload, filetype, baxiaTo
689
940
  }
690
941
  const resp = await fetch(joinUrl(baseUrl, '/api/v2/files/parse'), {
691
942
  method: 'POST',
692
- headers: qwenCommonHeaders(baxiaTokens, authHeaders),
943
+ headers: qwenCommonHeaders(baxiaTokens, authHeaders, { baseUrl, refererMode: 'web', acceptMode: 'default' }),
693
944
  body: JSON.stringify({ file_id: fileId })
694
945
  });
695
946
  const text = await resp.text().catch(() => '');
@@ -730,10 +981,17 @@ export async function uploadAttachments(args) {
730
981
  export function extractQwenChatPayload(request) {
731
982
  const container = isRecord(request) ? request : {};
732
983
  const payload = isRecord(container.data) ? container.data : container;
733
- const model = normalizeInputString(payload.model) || 'qwen3.5-plus';
734
- const messages = Array.isArray(payload.messages) ? payload.messages : [];
984
+ const model = normalizeInputString(payload.model) || 'qwen3.6-plus';
985
+ const directMessages = Array.isArray(payload.messages) ? payload.messages : [];
986
+ const compatPrompt = normalizeInputString(payload.prompt);
987
+ const messages = directMessages.length > 0
988
+ ? directMessages
989
+ : compatPrompt
990
+ ? [{ role: 'user', content: compatPrompt }]
991
+ : [];
735
992
  const streamFlag = payload.stream;
736
- const stream = streamFlag === undefined ? true : streamFlag === true;
993
+ // Follow OpenAI semantics: stream defaults to false unless explicitly true.
994
+ const stream = streamFlag === true;
737
995
  const metadata = isRecord(payload.metadata)
738
996
  ? payload.metadata
739
997
  : isRecord(container.metadata)
@@ -748,12 +1006,29 @@ export function extractQwenChatPayload(request) {
748
1006
  };
749
1007
  }
750
1008
  export function shouldUseSearchMode(payload) {
1009
+ if (parseQwenImageGenerationOptions(payload.metadata).enabled) {
1010
+ return false;
1011
+ }
751
1012
  if (payload.metadata?.qwenWebSearch === true || payload.metadata?.webSearch === true) {
752
1013
  return true;
753
1014
  }
754
1015
  return false;
755
1016
  }
1017
+ function shouldUseImageGenerationMode(payload) {
1018
+ return parseQwenImageGenerationOptions(payload.metadata).enabled;
1019
+ }
756
1020
  function extractChatIdFromCreatePayload(payload) {
1021
+ const readChatIdFromText = (value) => {
1022
+ const text = normalizeInputString(value);
1023
+ if (!text) {
1024
+ return '';
1025
+ }
1026
+ const namedMatch = text.match(/(?:chat[_-]?id|session[_-]?id|conversation[_-]?id)\s*[:=]\s*["']?([a-zA-Z0-9._:-]{8,})["']?/i);
1027
+ if (namedMatch?.[1]) {
1028
+ return namedMatch[1].trim();
1029
+ }
1030
+ return '';
1031
+ };
757
1032
  const readDirect = (node) => {
758
1033
  if (!node) {
759
1034
  return '';
@@ -764,27 +1039,122 @@ function extractChatIdFromCreatePayload(payload) {
764
1039
  normalizeInputString(node.session_id) ||
765
1040
  normalizeInputString(node.sessionId) ||
766
1041
  normalizeInputString(node.conversation_id) ||
767
- normalizeInputString(node.conversationId));
1042
+ normalizeInputString(node.conversationId) ||
1043
+ readChatIdFromText(node.details) ||
1044
+ readChatIdFromText(node.message) ||
1045
+ readChatIdFromText(node.msg));
768
1046
  };
769
1047
  const root = payload;
770
1048
  const dataNode = isRecord(root.data) ? root.data : undefined;
771
1049
  const resultNode = isRecord(root.result) ? root.result : undefined;
772
1050
  const chatNode = isRecord(dataNode?.chat) ? dataNode.chat : undefined;
773
- return readDirect(dataNode) || readDirect(chatNode) || readDirect(resultNode) || readDirect(root);
1051
+ const dataDetailsNode = isRecord(dataNode?.details) ? dataNode.details : undefined;
1052
+ const resultDetailsNode = isRecord(resultNode?.details) ? resultNode.details : undefined;
1053
+ const rootDetailsNode = isRecord(root.details) ? root.details : undefined;
1054
+ const nestedDataNode = isRecord(dataNode?.data) ? dataNode.data : undefined;
1055
+ const nestedResultNode = isRecord(dataNode?.result) ? dataNode.result : undefined;
1056
+ const conversationNode = isRecord(dataNode?.conversation) ? dataNode.conversation : undefined;
1057
+ const sessionNode = isRecord(dataNode?.session) ? dataNode.session : undefined;
1058
+ return (readDirect(dataNode) ||
1059
+ readDirect(chatNode) ||
1060
+ readDirect(conversationNode) ||
1061
+ readDirect(sessionNode) ||
1062
+ readDirect(nestedDataNode) ||
1063
+ readDirect(nestedResultNode) ||
1064
+ readDirect(resultNode) ||
1065
+ readDirect(dataDetailsNode) ||
1066
+ readDirect(resultDetailsNode) ||
1067
+ readDirect(rootDetailsNode) ||
1068
+ readDirect(root));
774
1069
  }
775
1070
  function extractCreateErrorMessage(payload) {
776
- const parts = [
777
- normalizeInputString(payload.msg),
778
- normalizeInputString(payload.message),
779
- normalizeInputString(payload.error),
780
- isRecord(payload.error) ? normalizeInputString(payload.error.message) : ''
1071
+ const reason = extractQwenErrorMessage(payload);
1072
+ if (reason) {
1073
+ return reason;
1074
+ }
1075
+ const dataNode = isRecord(payload.data) ? payload.data : undefined;
1076
+ const detailsNode = isRecord(dataNode?.details) ? dataNode.details : undefined;
1077
+ const detailsParts = [
1078
+ detailsNode ? normalizeInputString(detailsNode.message) : '',
1079
+ detailsNode ? normalizeInputString(detailsNode.msg) : '',
1080
+ detailsNode ? normalizeInputString(detailsNode.details) : '',
1081
+ detailsNode ? normalizeInputString(detailsNode.template) : '',
1082
+ detailsNode ? normalizeInputString(detailsNode.code) : ''
781
1083
  ].filter(Boolean);
782
- return parts[0] || '';
1084
+ return detailsParts[0] || '';
1085
+ }
1086
+ function inferCreateSessionStatusCode(payload, reason) {
1087
+ const dataNode = isRecord(payload.data) ? payload.data : undefined;
1088
+ const normalized = [
1089
+ normalizeInputString(payload.code),
1090
+ normalizeInputString(dataNode?.code),
1091
+ reason
1092
+ ]
1093
+ .filter(Boolean)
1094
+ .join(' ')
1095
+ .toLowerCase();
1096
+ if (normalized.includes('ratelimit') ||
1097
+ normalized.includes('rate_limit') ||
1098
+ normalized.includes('daily usage limit') ||
1099
+ normalized.includes('too many request') ||
1100
+ normalized.includes('达到今日的使用上限') ||
1101
+ normalized.includes('请求过于频繁')) {
1102
+ return 429;
1103
+ }
1104
+ if (normalized.includes('forbidden') ||
1105
+ normalized.includes('permission denied') ||
1106
+ normalized.includes('没有权限') ||
1107
+ normalized.includes('无权限') ||
1108
+ normalized.includes('permission')) {
1109
+ return 403;
1110
+ }
1111
+ if (normalized.includes('unauthorized') ||
1112
+ normalized.includes('invalid token') ||
1113
+ normalized.includes('login') ||
1114
+ normalized.includes('auth') ||
1115
+ normalized.includes('未登录')) {
1116
+ return 401;
1117
+ }
1118
+ return 502;
1119
+ }
1120
+ function parseQwenUpstreamBusinessErrorFromRaw(rawPayload) {
1121
+ const trimmed = rawPayload.trim();
1122
+ if (!trimmed || trimmed.startsWith('data:')) {
1123
+ return null;
1124
+ }
1125
+ try {
1126
+ const parsed = JSON.parse(trimmed);
1127
+ if (!isRecord(parsed)) {
1128
+ return null;
1129
+ }
1130
+ const success = parsed.success;
1131
+ const hasErrorNode = isRecord(parsed.error);
1132
+ const hasCode = Boolean(normalizeInputString(parsed.code));
1133
+ // Only treat as business rejection when payload explicitly signals failure.
1134
+ if (success !== false && !hasErrorNode && !hasCode) {
1135
+ return null;
1136
+ }
1137
+ const reason = extractQwenErrorMessage(parsed) || normalizeInputString(parsed.message) || 'upstream rejected request';
1138
+ const statusCode = inferCreateSessionStatusCode(parsed, reason);
1139
+ const code = statusCode === 429 ? 'QWENCHAT_RATE_LIMITED' : 'QWENCHAT_COMPLETION_REJECTED';
1140
+ return {
1141
+ message: `QwenChat upstream rejected completion request: ${reason}`,
1142
+ statusCode,
1143
+ code
1144
+ };
1145
+ }
1146
+ catch {
1147
+ return null;
1148
+ }
783
1149
  }
784
1150
  export async function createQwenChatSession(args) {
785
1151
  const resp = await fetch(joinUrl(args.baseUrl, DEFAULT_QWENCHAT_CHAT_CREATE_ENDPOINT), {
786
1152
  method: 'POST',
787
- headers: qwenCommonHeaders(args.baxiaTokens, args.authHeaders),
1153
+ headers: qwenCommonHeaders(args.baxiaTokens, args.authHeaders, {
1154
+ baseUrl: args.baseUrl,
1155
+ refererMode: 'guest',
1156
+ acceptMode: 'json'
1157
+ }),
788
1158
  body: JSON.stringify({
789
1159
  title: '新建对话',
790
1160
  models: [args.model],
@@ -803,13 +1173,27 @@ export async function createQwenChatSession(args) {
803
1173
  err.code = 'QWENCHAT_CREATE_SESSION_FAILED';
804
1174
  throw err;
805
1175
  }
1176
+ const chatId = extractChatIdFromCreatePayload(parsed.data);
806
1177
  const success = parsed.data.success === true;
807
- const chatId = success ? extractChatIdFromCreatePayload(parsed.data) : '';
1178
+ if (chatId) {
1179
+ return chatId;
1180
+ }
1181
+ if (parsed.data.success === false) {
1182
+ const reason = extractCreateErrorMessage(parsed.data);
1183
+ const dataNode = isRecord(parsed.data.data) ? parsed.data.data : undefined;
1184
+ const dataKeys = dataNode ? Object.keys(dataNode).slice(0, 8).join(',') : '';
1185
+ const detail = [reason, dataKeys ? `keys=${dataKeys}` : ''].filter(Boolean).join(' ');
1186
+ const suffix = detail ? ` (${detail})` : '';
1187
+ const err = new Error(`Failed to create qwenchat session: upstream rejected request${suffix}`);
1188
+ err.statusCode = inferCreateSessionStatusCode(parsed.data, reason);
1189
+ err.code = 'QWENCHAT_CREATE_SESSION_REJECTED';
1190
+ throw err;
1191
+ }
808
1192
  if (!chatId) {
809
1193
  const reason = extractCreateErrorMessage(parsed.data);
810
1194
  const dataNode = isRecord(parsed.data.data) ? parsed.data.data : undefined;
811
1195
  const dataKeys = dataNode ? Object.keys(dataNode).slice(0, 8).join(',') : '';
812
- const detail = reason || (dataKeys ? `keys=${dataKeys}` : '');
1196
+ const detail = reason || (dataKeys ? `keys=${dataKeys}` : '') || (success ? 'success=true' : '');
813
1197
  const suffix = detail ? ` (${detail})` : '';
814
1198
  const err = new Error(`Failed to create qwenchat session: missing chat id${suffix}`);
815
1199
  err.statusCode = 502;
@@ -819,7 +1203,7 @@ export async function createQwenChatSession(args) {
819
1203
  return chatId;
820
1204
  }
821
1205
  export function buildQwenChatCompletionRequest(args) {
822
- return {
1206
+ const request = {
823
1207
  stream: true,
824
1208
  version: '2.1',
825
1209
  incremental_output: true,
@@ -855,6 +1239,10 @@ export function buildQwenChatCompletionRequest(args) {
855
1239
  ],
856
1240
  timestamp: Date.now()
857
1241
  };
1242
+ if (args.chatType === 't2i' && args.imageSize) {
1243
+ request.size = args.imageSize;
1244
+ }
1245
+ return request;
858
1246
  }
859
1247
  function extractReasoningContentFromDelta(delta) {
860
1248
  const direct = normalizeInputString(delta.reasoning_content || delta.reasoning);
@@ -942,30 +1330,6 @@ function createOpenAiChunk(args) {
942
1330
  }
943
1331
  return chunk;
944
1332
  }
945
- function looksLikeToolMarkupNoise(text) {
946
- const normalized = normalizeInputString(text);
947
- if (!normalized) {
948
- return false;
949
- }
950
- return (/<\s*\/?\s*function_calls\b/i.test(normalized) ||
951
- /<\s*\/?\s*tool_call\b/i.test(normalized) ||
952
- /<\s*\/?\s*tool_name\b/i.test(normalized) ||
953
- /<\s*\/?\s*arg_key\b/i.test(normalized) ||
954
- /<\s*\/?\s*arg_value\b/i.test(normalized) ||
955
- /"tool_calls"\s*:/i.test(normalized));
956
- }
957
- function sanitizeDeltaForStreamEmit(delta) {
958
- const next = { ...delta };
959
- const content = normalizeInputString(next.content);
960
- if (content && looksLikeToolMarkupNoise(content)) {
961
- delete next.content;
962
- }
963
- const reasoning = normalizeInputString(next.reasoning_content);
964
- if (reasoning && looksLikeToolMarkupNoise(reasoning)) {
965
- delete next.reasoning_content;
966
- }
967
- return next;
968
- }
969
1333
  function processQwenSsePayloadLines(args) {
970
1334
  for (const line of args.payload.split('\n')) {
971
1335
  const trimmed = line.trimStart();
@@ -1001,8 +1365,15 @@ function processQwenSsePayloadLines(args) {
1001
1365
  if (reasoning)
1002
1366
  args.collect.reasoningParts.push(reasoning);
1003
1367
  }
1368
+ if (args.collect && deltaNode) {
1369
+ const phase = normalizeInputString(deltaNode.phase);
1370
+ const rawContent = normalizeInputString(deltaNode.content);
1371
+ if (phase === 'image_gen' && /^https?:\/\//i.test(rawContent)) {
1372
+ args.collect.imageUrls.push(rawContent);
1373
+ }
1374
+ }
1004
1375
  if (delta || finishReason || usage) {
1005
- const deltaForEmit = delta ? sanitizeDeltaForStreamEmit(delta) : {};
1376
+ const deltaForEmit = delta ? { ...delta } : {};
1006
1377
  args.onChunk(createOpenAiChunk({
1007
1378
  id: args.responseId,
1008
1379
  created: args.created,
@@ -1026,6 +1397,7 @@ export function createOpenAiMappedSseStream(input) {
1026
1397
  let buffer = '';
1027
1398
  const contentParts = [];
1028
1399
  const reasoningParts = [];
1400
+ const imageUrls = [];
1029
1401
  const usageRef = {};
1030
1402
  let lastUpstreamFinishReason = null;
1031
1403
  const writeDone = () => {
@@ -1051,7 +1423,7 @@ export function createOpenAiMappedSseStream(input) {
1051
1423
  lastUpstreamFinishReason = reason;
1052
1424
  },
1053
1425
  includeFinishReason: false,
1054
- collect: { contentParts, reasoningParts, usageRef },
1426
+ collect: { contentParts, reasoningParts, imageUrls, usageRef },
1055
1427
  responseId,
1056
1428
  created,
1057
1429
  model: input.model
@@ -1060,6 +1432,19 @@ export function createOpenAiMappedSseStream(input) {
1060
1432
  });
1061
1433
  input.upstreamStream.on('end', () => {
1062
1434
  if (buffer.trim()) {
1435
+ const upstreamBusinessError = parseQwenUpstreamBusinessErrorFromRaw(buffer);
1436
+ if (upstreamBusinessError) {
1437
+ output.write(`data: ${JSON.stringify({
1438
+ error: {
1439
+ message: upstreamBusinessError.message,
1440
+ code: upstreamBusinessError.code,
1441
+ status: upstreamBusinessError.statusCode
1442
+ }
1443
+ })}\n\n`);
1444
+ writeDone();
1445
+ output.end();
1446
+ return;
1447
+ }
1063
1448
  processQwenSsePayloadLines({
1064
1449
  payload: buffer,
1065
1450
  onChunk: (mappedChunk) => {
@@ -1072,13 +1457,17 @@ export function createOpenAiMappedSseStream(input) {
1072
1457
  lastUpstreamFinishReason = reason;
1073
1458
  },
1074
1459
  includeFinishReason: false,
1075
- collect: { contentParts, reasoningParts, usageRef },
1460
+ collect: { contentParts, reasoningParts, imageUrls, usageRef },
1076
1461
  responseId,
1077
1462
  created,
1078
1463
  model: input.model
1079
1464
  });
1080
1465
  }
1081
- const harvested = applyStandardizedTextHarvestToChatPayload({
1466
+ const dedupImageUrls = Array.from(new Set(imageUrls));
1467
+ const aggregatedContent = dedupImageUrls.length > 0
1468
+ ? dedupImageUrls.join('\n')
1469
+ : contentParts.join('');
1470
+ const harvested = applyStandardToolTextHarvestToChatPayload({
1082
1471
  id: responseId,
1083
1472
  object: 'chat.completion',
1084
1473
  created,
@@ -1089,7 +1478,7 @@ export function createOpenAiMappedSseStream(input) {
1089
1478
  finish_reason: 'stop',
1090
1479
  message: {
1091
1480
  role: 'assistant',
1092
- content: contentParts.join(''),
1481
+ content: aggregatedContent,
1093
1482
  ...(reasoningParts.length ? { reasoning_content: reasoningParts.join('') } : {})
1094
1483
  }
1095
1484
  }
@@ -1132,6 +1521,7 @@ export function createOpenAiMappedSseStream(input) {
1132
1521
  export async function collectQwenSseAsOpenAiResult(args) {
1133
1522
  const contentParts = [];
1134
1523
  const reasoningParts = [];
1524
+ const imageUrls = [];
1135
1525
  const usageRef = {};
1136
1526
  let buffer = '';
1137
1527
  await new Promise((resolve, reject) => {
@@ -1141,6 +1531,13 @@ export async function collectQwenSseAsOpenAiResult(args) {
1141
1531
  args.upstreamStream.on('end', resolve);
1142
1532
  args.upstreamStream.on('error', reject);
1143
1533
  });
1534
+ const upstreamBusinessError = parseQwenUpstreamBusinessErrorFromRaw(buffer);
1535
+ if (upstreamBusinessError) {
1536
+ const err = new Error(upstreamBusinessError.message);
1537
+ err.statusCode = upstreamBusinessError.statusCode;
1538
+ err.code = upstreamBusinessError.code;
1539
+ throw err;
1540
+ }
1144
1541
  processQwenSsePayloadLines({
1145
1542
  payload: buffer,
1146
1543
  onChunk: () => {
@@ -1149,11 +1546,15 @@ export async function collectQwenSseAsOpenAiResult(args) {
1149
1546
  onDone: () => {
1150
1547
  // no-op in aggregate mode
1151
1548
  },
1152
- collect: { contentParts, reasoningParts, usageRef },
1549
+ collect: { contentParts, reasoningParts, imageUrls, usageRef },
1153
1550
  responseId: `chatcmpl-${randomUUID()}`,
1154
1551
  created: Math.floor(Date.now() / 1000),
1155
1552
  model: args.model
1156
1553
  });
1554
+ const dedupImageUrls = Array.from(new Set(imageUrls));
1555
+ const aggregatedContent = dedupImageUrls.length > 0
1556
+ ? dedupImageUrls.join('\n')
1557
+ : contentParts.join('');
1157
1558
  const result = {
1158
1559
  id: `chatcmpl-${randomUUID()}`,
1159
1560
  object: 'chat.completion',
@@ -1164,7 +1565,7 @@ export async function collectQwenSseAsOpenAiResult(args) {
1164
1565
  index: 0,
1165
1566
  message: {
1166
1567
  role: 'assistant',
1167
- content: contentParts.join(''),
1568
+ content: aggregatedContent,
1168
1569
  ...(reasoningParts.length ? { reasoning_content: reasoningParts.join('') } : {})
1169
1570
  },
1170
1571
  finish_reason: 'stop'
@@ -1174,7 +1575,22 @@ export async function collectQwenSseAsOpenAiResult(args) {
1174
1575
  if (usageRef.usage) {
1175
1576
  result.usage = usageRef.usage;
1176
1577
  }
1177
- return applyStandardizedTextHarvestToChatPayload(result);
1578
+ const harvested = applyStandardToolTextHarvestToChatPayload(result);
1579
+ const firstChoice = Array.isArray(harvested.choices) && harvested.choices.length > 0 && isRecord(harvested.choices[0])
1580
+ ? harvested.choices[0]
1581
+ : undefined;
1582
+ const messageNode = firstChoice && isRecord(firstChoice.message) ? firstChoice.message : undefined;
1583
+ const finishReason = normalizeInputString(firstChoice?.finish_reason) || 'stop';
1584
+ const content = normalizeInputString(messageNode?.content);
1585
+ const reasoning = normalizeInputString(messageNode?.reasoning_content);
1586
+ const toolCalls = messageNode && Array.isArray(messageNode.tool_calls) ? messageNode.tool_calls : [];
1587
+ if (finishReason === 'stop' && !content && !reasoning && toolCalls.length === 0) {
1588
+ const err = new Error('QwenChat upstream returned an empty assistant message');
1589
+ err.statusCode = 502;
1590
+ err.code = 'QWENCHAT_EMPTY_ASSISTANT';
1591
+ throw err;
1592
+ }
1593
+ return harvested;
1178
1594
  }
1179
1595
  export async function buildQwenChatSendPlan(input) {
1180
1596
  const normalizedPayload = applyStandardToolTextRequest(input.payload);
@@ -1184,7 +1600,12 @@ export async function buildQwenChatSendPlan(input) {
1184
1600
  err.code = 'QWENCHAT_INVALID_REQUEST';
1185
1601
  throw err;
1186
1602
  }
1187
- const chatType = shouldUseSearchMode(normalizedPayload) ? 'search' : 't2t';
1603
+ const imageGenOptions = parseQwenImageGenerationOptions(normalizedPayload.metadata);
1604
+ const chatType = shouldUseImageGenerationMode(normalizedPayload)
1605
+ ? 't2i'
1606
+ : shouldUseSearchMode(normalizedPayload)
1607
+ ? 'search'
1608
+ : 't2t';
1188
1609
  const chatId = await createQwenChatSession({
1189
1610
  baseUrl: input.baseUrl,
1190
1611
  model: normalizedPayload.model,
@@ -1193,6 +1614,9 @@ export async function buildQwenChatSendPlan(input) {
1193
1614
  authHeaders: input.authHeaders
1194
1615
  });
1195
1616
  const parsedMessages = parseIncomingMessages(normalizedPayload.messages);
1617
+ const finalContent = chatType === 't2i' && imageGenOptions.count > 1
1618
+ ? `${parsedMessages.content}\n\n(Generate ${imageGenOptions.count} images.)`
1619
+ : parsedMessages.content;
1196
1620
  const uploaded = parsedMessages.attachments.length
1197
1621
  ? await uploadAttachments({
1198
1622
  baseUrl: input.baseUrl,
@@ -1204,14 +1628,18 @@ export async function buildQwenChatSendPlan(input) {
1204
1628
  const completionBody = buildQwenChatCompletionRequest({
1205
1629
  chatId,
1206
1630
  model: normalizedPayload.model,
1207
- content: parsedMessages.content,
1631
+ content: finalContent,
1208
1632
  uploadedFiles: uploaded.files,
1209
- chatType
1633
+ chatType,
1634
+ ...(chatType === 't2i' ? { imageSize: imageGenOptions.sizeRatio } : {})
1210
1635
  });
1211
1636
  const completionHeaders = {
1212
- ...qwenCommonHeaders(input.baxiaTokens, input.authHeaders),
1213
- version: '0.2.9',
1214
- Referer: `${input.baseUrl.replace(/\/$/, '')}/c/guest`
1637
+ ...qwenCommonHeaders(input.baxiaTokens, input.authHeaders, {
1638
+ baseUrl: input.baseUrl,
1639
+ refererMode: 'guest',
1640
+ acceptMode: 'json'
1641
+ }),
1642
+ version: '0.2.9'
1215
1643
  };
1216
1644
  return {
1217
1645
  completionUrl: `${joinUrl(input.baseUrl, DEFAULT_QWENCHAT_COMPLETION_ENDPOINT)}?chat_id=${encodeURIComponent(chatId)}`,
@@ -1223,29 +1651,27 @@ function applyStandardToolTextRequest(payload) {
1223
1651
  if (!Array.isArray(payload.tools) || payload.tools.length === 0) {
1224
1652
  return payload;
1225
1653
  }
1226
- try {
1227
- const transformed = applyStandardToolTextRequestTransform({
1228
- model: payload.model,
1229
- messages: payload.messages,
1230
- tools: payload.tools,
1231
- metadata: payload.metadata
1232
- }, {
1233
- providerProtocol: 'openai-chat',
1234
- entryEndpoint: '/v1/chat/completions'
1235
- });
1236
- const prompt = normalizeInputString(transformed.prompt);
1237
- if (!prompt) {
1238
- return payload;
1239
- }
1240
- return {
1241
- ...payload,
1242
- messages: [{ role: 'user', content: prompt }],
1243
- tools: undefined
1244
- };
1245
- }
1246
- catch {
1247
- return payload;
1654
+ const transformed = applyStandardToolTextRequestTransform({
1655
+ model: payload.model,
1656
+ messages: payload.messages,
1657
+ tools: payload.tools,
1658
+ metadata: payload.metadata
1659
+ }, {
1660
+ providerProtocol: 'openai-chat',
1661
+ entryEndpoint: '/v1/chat/completions'
1662
+ });
1663
+ const prompt = normalizeInputString(transformed.prompt);
1664
+ if (!prompt) {
1665
+ const err = new Error('QwenChat tool-text transform failed: prompt is empty');
1666
+ err.statusCode = 422;
1667
+ err.code = 'QWENCHAT_TOOL_TEXT_TRANSFORM_FAILED';
1668
+ throw err;
1248
1669
  }
1670
+ return {
1671
+ ...payload,
1672
+ messages: [{ role: 'user', content: prompt }],
1673
+ tools: undefined
1674
+ };
1249
1675
  }
1250
1676
  export function extractForwardAuthHeaders(headers) {
1251
1677
  const out = {};
@@ -1269,13 +1695,4 @@ export function classifyQwenChatProviderIdentity(input) {
1269
1695
  value.includes('qwenchat') ||
1270
1696
  value === 'chat:qwenchat-web');
1271
1697
  }
1272
- function applyStandardizedTextHarvestToChatPayload(payload) {
1273
- try {
1274
- const harvested = processChatResponseTools(payload);
1275
- return isRecord(harvested) ? harvested : payload;
1276
- }
1277
- catch {
1278
- return payload;
1279
- }
1280
- }
1281
1698
  //# sourceMappingURL=qwenchat-http-provider-helpers.js.map