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
@@ -1,241 +1,241 @@
1
- import { config, type ChatMode } from "../../core/config.ts";
2
- import { ContextLengthExceededError, ValidationError } from "../../core/errors.ts";
3
- import { getModelContextWindow } from "../../core/model-registry.ts";
4
- import {
5
- assertPromptWithinLimits,
6
- isRequestPersonalizationWithinLimit,
7
- } from "../../core/prompt-limits.ts";
8
- import type { Message } from "../../utils/types.ts";
9
- import { estimateTokenCount } from "../../utils/context-truncation.ts";
10
- import { deriveSessionId } from "../../utils/session-id.ts";
11
- import { getLogicalThreadState, consumeToolCapNotice } from "../../services/qwen.ts";
12
-
13
- export { estimateTokenCount, getModelContextWindow, deriveSessionId };
14
-
15
- export interface FinalContext {
16
- finalPrompt: string;
17
- sessionId: string | null;
18
- existingThread: boolean;
19
- shouldResetUpstreamThread: boolean;
20
- isNewSession: boolean;
21
- useThreadNative: boolean;
22
- updateLogicalThread: boolean;
23
- chatMode: ChatMode;
24
- isThinkingModel: boolean;
25
- estimatedTokens: number;
26
- modelContextWindow: number;
27
- isTitleGenerationRequest: boolean;
28
- requestPersonalizationInstruction: string | null;
29
- hasExplicitConversationKey: boolean;
30
- allowThreadReuse: boolean;
31
- }
32
-
33
- export interface BuildContextParams {
34
- messages: Message[];
35
- systemPrompt: string;
36
- toolInstructions: string;
37
- prompt: string;
38
- currentPrompt: string;
39
- modelId: string;
40
- enableThinking: boolean;
41
- conversationKey: string | null;
42
- hasExplicitConversationKey: boolean;
43
- chatMode?: ChatMode;
44
- }
45
-
46
- export async function buildFinalContext(
47
- params: BuildContextParams,
48
- ): Promise<FinalContext> {
49
- const {
50
- messages,
51
- systemPrompt,
52
- toolInstructions,
53
- prompt,
54
- currentPrompt,
55
- modelId,
56
- enableThinking,
57
- conversationKey,
58
- hasExplicitConversationKey,
59
- chatMode = "thread",
60
- } = params;
61
-
62
- const modelContextWindow = getModelContextWindow(modelId);
63
- const useThreadNative = true;
64
- const isTempMode = chatMode === "temp";
65
- // A continuation is ANY evidence of a prior turn, not just a plain
66
- // role:"assistant" message. Tool-loop clients (Zed/Cline) can send history
67
- // with tool/function responses or assistant tool_calls but WITHOUT a plain
68
- // assistant entry; misclassifying those as a new session forced the FULL
69
- // history to be re-sent on every request (and every chat_in_progress retry)
70
- // instead of the thread-native delta.
71
- // In temp mode EVERY request is a new (ephemeral) chat, so the whole history
72
- // is always sent and no thread state is ever consulted.
73
- const isNewSession = isTempMode
74
- ? true
75
- : !messages.some(isContinuationMessage);
76
- const completeInstructions = [systemPrompt.trim(), toolInstructions.trim()]
77
- .filter(Boolean)
78
- .join("\n\n");
79
-
80
- // Thread reuse is allowed when:
81
- // 1. Thread-native mode is active
82
- // 2. Either: explicit session_id/conversation_id was provided
83
- // OR: this is a continuation (has assistant messages in history)
84
- // This prevents new IDE chats from accidentally reusing old Qwen chats
85
- // while still allowing continuations without explicit session_id
86
- const allowThreadReuse = isTempMode
87
- ? false
88
- : useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
89
-
90
- // Compute sessionId: only generate a persistent session ID when we have
91
- // an explicit conversation key. Otherwise, generate an ephemeral ID for
92
- // logging/metrics only (not used for thread reuse). Temp mode never persists
93
- // a thread, so it has no session id.
94
- const sessionId = isTempMode
95
- ? null
96
- : (conversationKey || useThreadNative)
97
- ? deriveSessionId(
98
- messages,
99
- conversationKey ? completeInstructions : "",
100
- conversationKey ?? "implicit-thread",
101
- )
102
- : null;
103
-
104
- // Only load existing thread when reuse is allowed
105
- const existingThread = allowThreadReuse
106
- ? getLogicalThreadState(sessionId)
107
- : null;
108
-
109
- const hasTrailingToolResult = detectTrailingToolResult(messages);
110
- // Thread-native: send full history when Qwen has no context yet, but preserve
111
- // tool-result deltas because the upstream parent chain already owns the call.
112
- // Temp mode: always send the FULL history (OpenAI standard).
113
- const baseActivePrompt = isTempMode
114
- ? prompt
115
- : (!existingThread && !hasTrailingToolResult ? prompt : currentPrompt) ||
116
- prompt;
117
-
118
- // If the previous turn of this session was closed early at the per-turn
119
- // tool-call cap, tell the model so it knows calls beyond the cap were NOT
120
- // executed and can re-issue them. The notice is consumed once (it clears
121
- // itself) and rides this single turn only. This is a transient system notice,
122
- // not the persistent personalization instruction, so it may live in the prompt.
123
- const toolCapNotice = consumeToolCapNotice(sessionId)
124
- ? `[SYSTEM NOTICE] Your previous response reached the maximum of ${config.retry.maxToolCallsPerTurn} tool calls per turn; any tool calls beyond that limit were NOT executed. Review the tool results below and continue; if you intended more operations, issue them now in smaller batches.\n\n`
125
- : "";
126
- const activePrompt = toolCapNotice
127
- ? toolCapNotice + baseActivePrompt
128
- : baseActivePrompt;
129
- const isTitleGenerationRequest = detectTitleGenerationRequest(messages);
130
- const requestedPersonalization =
131
- config.qwen.personalizationFromRequest && !isTitleGenerationRequest;
132
- const personalizationInstruction = completeInstructions;
133
- const useRequestPersonalization =
134
- requestedPersonalization &&
135
- isRequestPersonalizationWithinLimit(personalizationInstruction);
136
-
137
- // Agent instructions and tools ride ONLY the account-level personalization
138
- // (confirmed before the completion request is sent — the real Qwen client
139
- // also never sends a system prompt in the completions payload). When the
140
- // channel cannot carry them, fail loud instead of degrading to inline.
141
- if (completeInstructions && !useRequestPersonalization) {
142
- if (requestedPersonalization) {
143
- throw new ContextLengthExceededError(
144
- `System instructions and tools (${Buffer.byteLength(personalizationInstruction, "utf8")} bytes) exceed the personalization payload limit (${config.qwen.maxPersonalizationBytes} bytes) and are no longer sent inline. Raise QWEN_MAX_PERSONALIZATION_BYTES or reduce the instruction size.`,
145
- );
146
- }
147
- if (!isTitleGenerationRequest) {
148
- throw new ValidationError(
149
- "Agent instructions can only be delivered via Qwen account personalization, but QWEN_PERSONALIZATION_FROM_REQUEST is disabled. Re-enable it or remove the system instructions from the request.",
150
- );
151
- }
152
- }
153
- const estimatedTokens = estimateTokenCount(
154
- completeInstructions,
155
- activePrompt,
156
- );
157
- // Instructions are delivered exclusively via account-level personalization;
158
- // the prompt carries only the conversation. Title generation does not sync
159
- // personalization, so it keeps its (small) instructions inline.
160
- const finalPrompt =
161
- isTitleGenerationRequest && completeInstructions
162
- ? `${completeInstructions}\n${activePrompt}`
163
- : activePrompt;
164
-
165
- // Truncation is deferred to tryCreateStreamWithRetry, which runs after the
166
- // account is selected and the real model context window has been synced from
167
- // Qwen's /api/models catalog. The early context build only performs the byte
168
- // limit check; the authoritative token check happens downstream.
169
- assertPromptWithinLimits(finalPrompt, modelId, { checkModelContext: false });
170
-
171
- const isThinkingModel = enableThinking;
172
- const shouldResetUpstreamThread = false;
173
-
174
- return {
175
- finalPrompt,
176
- sessionId,
177
- existingThread: !!existingThread,
178
- shouldResetUpstreamThread,
179
- isNewSession,
180
- useThreadNative,
181
- // Thread state is only persisted in thread mode (temp chats are ephemeral).
182
- updateLogicalThread: isTempMode ? false : useThreadNative,
183
- chatMode,
184
- isThinkingModel,
185
- estimatedTokens,
186
- modelContextWindow,
187
- isTitleGenerationRequest,
188
- requestPersonalizationInstruction: useRequestPersonalization
189
- ? personalizationInstruction
190
- : null,
191
- hasExplicitConversationKey,
192
- allowThreadReuse,
193
- };
194
- }
195
-
196
- function isContinuationMessage(message: Message): boolean {
197
- return (
198
- message.role === "assistant" ||
199
- message.role === "tool" ||
200
- message.role === "function" ||
201
- (Array.isArray(message.tool_calls) && message.tool_calls.length > 0)
202
- );
203
- }
204
-
205
- function extractMessageText(message: Message | undefined): string {
206
- if (!message) return "";
207
- const content: unknown = message.content;
208
- if (typeof content === "string") return content;
209
- if (Array.isArray(content)) {
210
- return content
211
- .map((part: any) => (part?.type === "text" ? part.text || "" : ""))
212
- .join("\n");
213
- }
214
- if (content && typeof content === "object") return JSON.stringify(content);
215
- return "";
216
- }
217
-
218
- function detectTrailingToolResult(messages: Message[]): boolean {
219
- for (let i = messages.length - 1; i >= 0; i--) {
220
- const role = messages[i].role;
221
- if (role === "system") continue;
222
- return role === "tool" || role === "function";
223
- }
224
- return false;
225
- }
226
-
227
- function detectTitleGenerationRequest(messages: Message[]): boolean {
228
- if (messages.length < 2) return false;
229
- const last = messages[messages.length - 1];
230
- if (last?.role !== "user") return false;
231
-
232
- const text = extractMessageText(last).toLowerCase();
233
- if (!text) return false;
234
-
235
- // The first pattern is strictly subsumed by the second (which does not
236
- // require the leading verb), so it is redundant and was removed.
237
- return (
238
- /\btitle\b[\s\S]{0,80}\bconversation\b/.test(text) ||
239
- /\bconversation\b[\s\S]{0,80}\btitle\b/.test(text)
240
- );
241
- }
1
+ import { config, type ChatMode } from "../../core/config.ts";
2
+ import { ContextLengthExceededError, ValidationError } from "../../core/errors.ts";
3
+ import { getModelContextWindow } from "../../core/model-registry.ts";
4
+ import {
5
+ assertPromptWithinLimits,
6
+ isRequestPersonalizationWithinLimit,
7
+ } from "../../core/prompt-limits.ts";
8
+ import type { Message } from "../../utils/types.ts";
9
+ import { estimateTokenCount } from "../../utils/context-truncation.ts";
10
+ import { deriveSessionId } from "../../utils/session-id.ts";
11
+ import { getLogicalThreadState, consumeToolCapNotice } from "../../services/qwen.ts";
12
+
13
+ export { estimateTokenCount, getModelContextWindow, deriveSessionId };
14
+
15
+ export interface FinalContext {
16
+ finalPrompt: string;
17
+ sessionId: string | null;
18
+ existingThread: boolean;
19
+ shouldResetUpstreamThread: boolean;
20
+ isNewSession: boolean;
21
+ useThreadNative: boolean;
22
+ updateLogicalThread: boolean;
23
+ chatMode: ChatMode;
24
+ isThinkingModel: boolean;
25
+ estimatedTokens: number;
26
+ modelContextWindow: number;
27
+ isTitleGenerationRequest: boolean;
28
+ requestPersonalizationInstruction: string | null;
29
+ hasExplicitConversationKey: boolean;
30
+ allowThreadReuse: boolean;
31
+ }
32
+
33
+ export interface BuildContextParams {
34
+ messages: Message[];
35
+ systemPrompt: string;
36
+ toolInstructions: string;
37
+ prompt: string;
38
+ currentPrompt: string;
39
+ modelId: string;
40
+ enableThinking: boolean;
41
+ conversationKey: string | null;
42
+ hasExplicitConversationKey: boolean;
43
+ chatMode?: ChatMode;
44
+ }
45
+
46
+ export async function buildFinalContext(
47
+ params: BuildContextParams,
48
+ ): Promise<FinalContext> {
49
+ const {
50
+ messages,
51
+ systemPrompt,
52
+ toolInstructions,
53
+ prompt,
54
+ currentPrompt,
55
+ modelId,
56
+ enableThinking,
57
+ conversationKey,
58
+ hasExplicitConversationKey,
59
+ chatMode = "thread",
60
+ } = params;
61
+
62
+ const modelContextWindow = getModelContextWindow(modelId);
63
+ const useThreadNative = true;
64
+ const isTempMode = chatMode === "temp";
65
+ // A continuation is ANY evidence of a prior turn, not just a plain
66
+ // role:"assistant" message. Tool-loop clients (Zed/Cline) can send history
67
+ // with tool/function responses or assistant tool_calls but WITHOUT a plain
68
+ // assistant entry; misclassifying those as a new session forced the FULL
69
+ // history to be re-sent on every request (and every chat_in_progress retry)
70
+ // instead of the thread-native delta.
71
+ // In temp mode EVERY request is a new (ephemeral) chat, so the whole history
72
+ // is always sent and no thread state is ever consulted.
73
+ const isNewSession = isTempMode
74
+ ? true
75
+ : !messages.some(isContinuationMessage);
76
+ const completeInstructions = [systemPrompt.trim(), toolInstructions.trim()]
77
+ .filter(Boolean)
78
+ .join("\n\n");
79
+
80
+ // Thread reuse is allowed when:
81
+ // 1. Thread-native mode is active
82
+ // 2. Either: explicit session_id/conversation_id was provided
83
+ // OR: this is a continuation (has assistant messages in history)
84
+ // This prevents new IDE chats from accidentally reusing old Qwen chats
85
+ // while still allowing continuations without explicit session_id
86
+ const allowThreadReuse = isTempMode
87
+ ? false
88
+ : useThreadNative && (hasExplicitConversationKey || !isNewSession); // has assistant messages = continuation of existing chat
89
+
90
+ // Compute sessionId: only generate a persistent session ID when we have
91
+ // an explicit conversation key. Otherwise, generate an ephemeral ID for
92
+ // logging/metrics only (not used for thread reuse). Temp mode never persists
93
+ // a thread, so it has no session id.
94
+ const sessionId = isTempMode
95
+ ? null
96
+ : (conversationKey || useThreadNative)
97
+ ? deriveSessionId(
98
+ messages,
99
+ conversationKey ? completeInstructions : "",
100
+ conversationKey ?? "implicit-thread",
101
+ )
102
+ : null;
103
+
104
+ // Only load existing thread when reuse is allowed
105
+ const existingThread = allowThreadReuse
106
+ ? getLogicalThreadState(sessionId)
107
+ : null;
108
+
109
+ const hasTrailingToolResult = detectTrailingToolResult(messages);
110
+ // Thread-native: send full history when Qwen has no context yet, but preserve
111
+ // tool-result deltas because the upstream parent chain already owns the call.
112
+ // Temp mode: always send the FULL history (OpenAI standard).
113
+ const baseActivePrompt = isTempMode
114
+ ? prompt
115
+ : (!existingThread && !hasTrailingToolResult ? prompt : currentPrompt) ||
116
+ prompt;
117
+
118
+ // If the previous turn of this session was closed early at the per-turn
119
+ // tool-call cap, tell the model so it knows calls beyond the cap were NOT
120
+ // executed and can re-issue them. The notice is consumed once (it clears
121
+ // itself) and rides this single turn only. This is a transient system notice,
122
+ // not the persistent personalization instruction, so it may live in the prompt.
123
+ const toolCapNotice = consumeToolCapNotice(sessionId)
124
+ ? `[SYSTEM NOTICE] Your previous response reached the maximum of ${config.retry.maxToolCallsPerTurn} tool calls per turn; any tool calls beyond that limit were NOT executed. Review the tool results below and continue; if you intended more operations, issue them now in smaller batches.\n\n`
125
+ : "";
126
+ const activePrompt = toolCapNotice
127
+ ? toolCapNotice + baseActivePrompt
128
+ : baseActivePrompt;
129
+ const isTitleGenerationRequest = detectTitleGenerationRequest(messages);
130
+ const requestedPersonalization =
131
+ config.qwen.personalizationFromRequest && !isTitleGenerationRequest;
132
+ const personalizationInstruction = completeInstructions;
133
+ const useRequestPersonalization =
134
+ requestedPersonalization &&
135
+ isRequestPersonalizationWithinLimit(personalizationInstruction);
136
+
137
+ // Agent instructions and tools ride ONLY the account-level personalization
138
+ // (confirmed before the completion request is sent — the real Qwen client
139
+ // also never sends a system prompt in the completions payload). When the
140
+ // channel cannot carry them, fail loud instead of degrading to inline.
141
+ if (completeInstructions && !useRequestPersonalization) {
142
+ if (requestedPersonalization) {
143
+ throw new ContextLengthExceededError(
144
+ `System instructions and tools (${Buffer.byteLength(personalizationInstruction, "utf8")} bytes) exceed the personalization payload limit (${config.qwen.maxPersonalizationBytes} bytes) and are no longer sent inline. Raise QWEN_MAX_PERSONALIZATION_BYTES or reduce the instruction size.`,
145
+ );
146
+ }
147
+ if (!isTitleGenerationRequest) {
148
+ throw new ValidationError(
149
+ "Agent instructions can only be delivered via Qwen account personalization, but QWEN_PERSONALIZATION_FROM_REQUEST is disabled. Re-enable it or remove the system instructions from the request.",
150
+ );
151
+ }
152
+ }
153
+ const estimatedTokens = estimateTokenCount(
154
+ completeInstructions,
155
+ activePrompt,
156
+ );
157
+ // Instructions are delivered exclusively via account-level personalization;
158
+ // the prompt carries only the conversation. Title generation does not sync
159
+ // personalization, so it keeps its (small) instructions inline.
160
+ const finalPrompt =
161
+ isTitleGenerationRequest && completeInstructions
162
+ ? `${completeInstructions}\n${activePrompt}`
163
+ : activePrompt;
164
+
165
+ // Truncation is deferred to tryCreateStreamWithRetry, which runs after the
166
+ // account is selected and the real model context window has been synced from
167
+ // Qwen's /api/models catalog. The early context build only performs the byte
168
+ // limit check; the authoritative token check happens downstream.
169
+ assertPromptWithinLimits(finalPrompt, modelId, { checkModelContext: false });
170
+
171
+ const isThinkingModel = enableThinking;
172
+ const shouldResetUpstreamThread = false;
173
+
174
+ return {
175
+ finalPrompt,
176
+ sessionId,
177
+ existingThread: !!existingThread,
178
+ shouldResetUpstreamThread,
179
+ isNewSession,
180
+ useThreadNative,
181
+ // Thread state is only persisted in thread mode (temp chats are ephemeral).
182
+ updateLogicalThread: isTempMode ? false : useThreadNative,
183
+ chatMode,
184
+ isThinkingModel,
185
+ estimatedTokens,
186
+ modelContextWindow,
187
+ isTitleGenerationRequest,
188
+ requestPersonalizationInstruction: useRequestPersonalization
189
+ ? personalizationInstruction
190
+ : null,
191
+ hasExplicitConversationKey,
192
+ allowThreadReuse,
193
+ };
194
+ }
195
+
196
+ function isContinuationMessage(message: Message): boolean {
197
+ return (
198
+ message.role === "assistant" ||
199
+ message.role === "tool" ||
200
+ message.role === "function" ||
201
+ (Array.isArray(message.tool_calls) && message.tool_calls.length > 0)
202
+ );
203
+ }
204
+
205
+ function extractMessageText(message: Message | undefined): string {
206
+ if (!message) return "";
207
+ const content: unknown = message.content;
208
+ if (typeof content === "string") return content;
209
+ if (Array.isArray(content)) {
210
+ return content
211
+ .map((part: any) => (part?.type === "text" ? part.text || "" : ""))
212
+ .join("\n");
213
+ }
214
+ if (content && typeof content === "object") return JSON.stringify(content);
215
+ return "";
216
+ }
217
+
218
+ function detectTrailingToolResult(messages: Message[]): boolean {
219
+ for (let i = messages.length - 1; i >= 0; i--) {
220
+ const role = messages[i].role;
221
+ if (role === "system") continue;
222
+ return role === "tool" || role === "function";
223
+ }
224
+ return false;
225
+ }
226
+
227
+ function detectTitleGenerationRequest(messages: Message[]): boolean {
228
+ if (messages.length < 2) return false;
229
+ const last = messages[messages.length - 1];
230
+ if (last?.role !== "user") return false;
231
+
232
+ const text = extractMessageText(last).toLowerCase();
233
+ if (!text) return false;
234
+
235
+ // The first pattern is strictly subsumed by the second (which does not
236
+ // require the leading verb), so it is redundant and was removed.
237
+ return (
238
+ /\btitle\b[\s\S]{0,80}\bconversation\b/.test(text) ||
239
+ /\bconversation\b[\s\S]{0,80}\btitle\b/.test(text)
240
+ );
241
+ }
@@ -1,85 +1,85 @@
1
- /**
2
- * Parse a non-SSE upstream body that may contain a Qwen error payload.
3
- * Returns null when the body is not a recognized error document.
4
- */
5
- export interface ParsedQwenErrorPayload {
6
- code: string;
7
- details: string;
8
- message: string;
9
- status: number;
10
- }
11
-
12
- function isWafChallenge(value: string): boolean {
13
- const normalized = value.toLowerCase();
14
- return (
15
- normalized.includes("aliyun_waf") ||
16
- normalized.includes("_____tmd_____") ||
17
- normalized.includes("fail_sys_user_validate") ||
18
- normalized.includes("rgv587_error") ||
19
- normalized.includes("denyfromx5") ||
20
- normalized.includes("captcha") ||
21
- normalized.includes("security verification")
22
- );
23
- }
24
-
25
- /**
26
- * Parse an upstream response that arrived before any SSE event. The returned
27
- * details are sanitized so an HTML WAF page is never sent back to API clients.
28
- */
29
- export function parseQwenErrorPayload(
30
- raw: string,
31
- ): ParsedQwenErrorPayload | null {
32
- const text = raw.trim();
33
- if (!text || text.startsWith("data:")) return null;
34
-
35
- try {
36
- const payload = JSON.parse(text);
37
- if (payload && payload.success === false) {
38
- const code = payload.data?.code || payload.code || "UpstreamError";
39
- const details =
40
- payload.data?.details || payload.message || "Qwen returned an error";
41
- const wait =
42
- payload.data?.num !== undefined
43
- ? ` Wait about ${payload.data.num} hour(s) before trying again.`
44
- : "";
45
- const status =
46
- code === "RateLimited" ? 429 : code === "Not_Found" ? 404 : 502;
47
- return {
48
- code,
49
- details,
50
- message: `Qwen upstream error: ${code}: ${details}.${wait}`,
51
- status,
52
- };
53
- }
54
- if (payload && payload.error) {
55
- const error = payload.error;
56
- const code =
57
- typeof error === "object" && error?.code
58
- ? error.code
59
- : payload.code || "UpstreamError";
60
- const details =
61
- typeof error === "string"
62
- ? error
63
- : error.details || error.message || JSON.stringify(error);
64
- return {
65
- code,
66
- details,
67
- message: `Qwen upstream error: ${code}: ${details}`,
68
- status: 502,
69
- };
70
- }
71
- } catch {
72
- const waf = isWafChallenge(text);
73
- const details = waf
74
- ? "Qwen returned an anti-bot challenge instead of an SSE response."
75
- : "Qwen returned a non-SSE response before generation started.";
76
- return {
77
- code: waf ? "waf_challenge" : "non_sse_response",
78
- details,
79
- message: `Qwen upstream error: ${details}`,
80
- status: 502,
81
- };
82
- }
83
-
84
- return null;
85
- }
1
+ /**
2
+ * Parse a non-SSE upstream body that may contain a Qwen error payload.
3
+ * Returns null when the body is not a recognized error document.
4
+ */
5
+ export interface ParsedQwenErrorPayload {
6
+ code: string;
7
+ details: string;
8
+ message: string;
9
+ status: number;
10
+ }
11
+
12
+ function isWafChallenge(value: string): boolean {
13
+ const normalized = value.toLowerCase();
14
+ return (
15
+ normalized.includes("aliyun_waf") ||
16
+ normalized.includes("_____tmd_____") ||
17
+ normalized.includes("fail_sys_user_validate") ||
18
+ normalized.includes("rgv587_error") ||
19
+ normalized.includes("denyfromx5") ||
20
+ normalized.includes("captcha") ||
21
+ normalized.includes("security verification")
22
+ );
23
+ }
24
+
25
+ /**
26
+ * Parse an upstream response that arrived before any SSE event. The returned
27
+ * details are sanitized so an HTML WAF page is never sent back to API clients.
28
+ */
29
+ export function parseQwenErrorPayload(
30
+ raw: string,
31
+ ): ParsedQwenErrorPayload | null {
32
+ const text = raw.trim();
33
+ if (!text || text.startsWith("data:")) return null;
34
+
35
+ try {
36
+ const payload = JSON.parse(text);
37
+ if (payload && payload.success === false) {
38
+ const code = payload.data?.code || payload.code || "UpstreamError";
39
+ const details =
40
+ payload.data?.details || payload.message || "Qwen returned an error";
41
+ const wait =
42
+ payload.data?.num !== undefined
43
+ ? ` Wait about ${payload.data.num} hour(s) before trying again.`
44
+ : "";
45
+ const status =
46
+ code === "RateLimited" ? 429 : code === "Not_Found" ? 404 : 502;
47
+ return {
48
+ code,
49
+ details,
50
+ message: `Qwen upstream error: ${code}: ${details}.${wait}`,
51
+ status,
52
+ };
53
+ }
54
+ if (payload && payload.error) {
55
+ const error = payload.error;
56
+ const code =
57
+ typeof error === "object" && error?.code
58
+ ? error.code
59
+ : payload.code || "UpstreamError";
60
+ const details =
61
+ typeof error === "string"
62
+ ? error
63
+ : error.details || error.message || JSON.stringify(error);
64
+ return {
65
+ code,
66
+ details,
67
+ message: `Qwen upstream error: ${code}: ${details}`,
68
+ status: 502,
69
+ };
70
+ }
71
+ } catch {
72
+ const waf = isWafChallenge(text);
73
+ const details = waf
74
+ ? "Qwen returned an anti-bot challenge instead of an SSE response."
75
+ : "Qwen returned a non-SSE response before generation started.";
76
+ return {
77
+ code: waf ? "waf_challenge" : "non_sse_response",
78
+ details,
79
+ message: `Qwen upstream error: ${details}`,
80
+ status: 502,
81
+ };
82
+ }
83
+
84
+ return null;
85
+ }