|
| 1 | +diff --git a/src/main/proxy/adapters/qwen-ai.ts b/src/main/proxy/adapters/qwen-ai.ts |
| 2 | +index 631639d..9b53e2f 100644 |
| 3 | +--- a/src/main/proxy/adapters/qwen-ai.ts |
| 4 | ++++ b/src/main/proxy/adapters/qwen-ai.ts |
| 5 | +@@ -9,6 +9,7 @@ import { PassThrough } from 'stream' |
| 6 | + import { createParser } from 'eventsource-parser' |
| 7 | + import { Account, Provider } from '../../store/types' |
| 8 | + import { hasToolUse, parseToolUse, ToolCall } from '../promptToolUse' |
| 9 | ++import { messagesToPrompt } from '../utils/messageToPrompt' |
| 10 | + |
| 11 | + const QWEN_AI_BASE = 'https://chat.qwen.ai' |
| 12 | + |
| 13 | +@@ -260,25 +261,10 @@ export class QwenAiAdapter { |
| 14 | + const chatId = await this.createChat(modelId, 'OpenAI_API_Chat') |
| 15 | + console.log('[QwenAI] Created new chat:', chatId) |
| 16 | + |
| 17 | +- const messages = request.messages |
| 18 | +- |
| 19 | +- // Extract system message and user message |
| 20 | +- let systemContent = '' |
| 21 | +- let userContent = '' |
| 22 | +- |
| 23 | +- // Single-turn mode: extract all messages |
| 24 | +- for (const msg of messages) { |
| 25 | +- if (msg.role === 'system') { |
| 26 | +- systemContent += (systemContent ? '\n\n' : '') + msg.content |
| 27 | +- } else if (msg.role === 'user') { |
| 28 | +- userContent = msg.content |
| 29 | +- } |
| 30 | +- } |
| 31 | +- |
| 32 | +- // If system prompt exists, prepend it to user content |
| 33 | +- if (systemContent) { |
| 34 | +- userContent = `${systemContent}\n\nUser: ${userContent}` |
| 35 | +- } |
| 36 | ++ // Convert entire conversation history to a single prompt string |
| 37 | ++ // This ensures multi-turn context is preserved across requests |
| 38 | ++ const userContent = messagesToPrompt(request.messages as any) |
| 39 | ++ console.log('[QwenAI] Converted conversation history to prompt, length:', userContent.length) |
| 40 | + |
| 41 | + const fid = uuid() |
| 42 | + const childId = uuid() |
| 43 | +diff --git a/src/main/proxy/adapters/qwen.ts b/src/main/proxy/adapters/qwen.ts |
| 44 | +index 5f289e0..e157b3f 100644 |
| 45 | +--- a/src/main/proxy/adapters/qwen.ts |
| 46 | ++++ b/src/main/proxy/adapters/qwen.ts |
| 47 | +@@ -20,6 +20,7 @@ import { |
| 48 | + createBaseChunk, |
| 49 | + ToolCallState |
| 50 | + } from '../utils/streamToolHandler' |
| 51 | ++import { messagesToPrompt, extractTextContent as extractTextContentUtil } from '../utils/messageToPrompt' |
| 52 | + |
| 53 | + /** |
| 54 | + * Check if content contains tool calls (both bracket and XML formats) |
| 55 | +@@ -70,7 +71,8 @@ interface ChatCompletionRequest { |
| 56 | + web_search?: boolean |
| 57 | + reasoning_effort?: 'low' | 'medium' | 'high' |
| 58 | + enableThinking?: boolean |
| 59 | +- enableWebSearch?: boolean |
| 60 | ++ enableWeb?: boolean |
| 61 | ++ originalModel?: string |
| 62 | + } |
| 63 | + |
| 64 | + function uuid(separator: boolean = true): string { |
| 65 | +@@ -152,15 +154,16 @@ export class QwenAdapter { |
| 66 | + const modelLower = modelForDetection.toLowerCase() |
| 67 | + |
| 68 | + let enableThinking = request.enableThinking ?? false |
| 69 | +- let enableWebSearch = request.enableWebSearch ?? false |
| 70 | ++ // Use web_search from request (OpenAI standard) and fallback to model name detection |
| 71 | ++ let enableWeb = request.web_search ?? false |
| 72 | + |
| 73 | + // Auto-enable based on model name (if not explicitly set) |
| 74 | + if (!enableThinking && (modelLower.includes('think') || modelLower.includes('r1'))) { |
| 75 | + enableThinking = true |
| 76 | + console.log('[Qwen] Thinking mode enabled (from model name)') |
| 77 | + } |
| 78 | +- if (!enableWebSearch && modelLower.includes('search')) { |
| 79 | +- enableWebSearch = true |
| 80 | ++ if (!enableWeb && modelLower.includes('')) { |
| 81 | ++ enableWeb = true |
| 82 | + console.log('[Qwen] Web search enabled (from model name)') |
| 83 | + } |
| 84 | + |
| 85 | +@@ -179,38 +182,21 @@ export class QwenAdapter { |
| 86 | + }) |
| 87 | + console.log('[Qwen] Using model:', actualModel) |
| 88 | + |
| 89 | +- // Find system message and user message |
| 90 | +- let systemPrompt = '' |
| 91 | +- let userContent = '' |
| 92 | +- |
| 93 | +- for (const msg of request.messages) { |
| 94 | +- if (msg.role === 'system') { |
| 95 | +- systemPrompt = extractTextContent(msg.content) |
| 96 | +- } else if (msg.role === 'user') { |
| 97 | +- userContent = extractTextContent(msg.content) |
| 98 | +- } |
| 99 | +- } |
| 100 | ++ // Convert entire conversation history to a single prompt string |
| 101 | ++ // This ensures multi-turn context is preserved across requests |
| 102 | ++ let finalContent = messagesToPrompt(request.messages as any) |
| 103 | + |
| 104 | + // Inject tools prompt if tools are provided and not already injected by client |
| 105 | + if (request.tools && request.tools.length > 0 && !hasToolPromptInjected(request.messages)) { |
| 106 | + const toolsPrompt = toolsToSystemPrompt(request.tools) |
| 107 | +- systemPrompt = systemPrompt |
| 108 | +- ? systemPrompt + '\n\n' + toolsPrompt |
| 109 | +- : toolsPrompt |
| 110 | +- // Add tool wrap hint to user content |
| 111 | +- userContent = userContent + TOOL_WRAP_HINT |
| 112 | ++ finalContent = toolsPrompt + '\n\n' + finalContent + TOOL_WRAP_HINT |
| 113 | + } |
| 114 | + |
| 115 | +- // If system prompt exists, prepend it to user content |
| 116 | +- const finalContent = systemPrompt |
| 117 | +- ? `${systemPrompt}\n\nUser: ${userContent}` |
| 118 | +- : userContent |
| 119 | +- |
| 120 | + const timestamp = Date.now() |
| 121 | + const nonce = generateNonce() |
| 122 | + |
| 123 | + const requestBody = { |
| 124 | +- deep_search: (enableWebSearch || enableThinking) ? '1' : '0', |
| 125 | ++ deep_search: (enableWeb || enableThinking) ? '1' : '0', |
| 126 | + req_id: reqId, |
| 127 | + model: actualModel, |
| 128 | + scene: 'chat', |
| 129 | +@@ -228,7 +214,7 @@ export class QwenAdapter { |
| 130 | + ], |
| 131 | + from: 'default', |
| 132 | + parent_req_id: '0', |
| 133 | +- enable_search: enableWebSearch, |
| 134 | ++ enable_search: enableWeb, |
| 135 | + biz_data: '{"entryPoint":"tongyigw"}', |
| 136 | + scene_param: 'first_turn', |
| 137 | + chat_client: 'h5', |
| 138 | +@@ -398,7 +384,9 @@ export class QwenStreamHandler { |
| 139 | + |
| 140 | + console.log('[Qwen] Starting stream handler...') |
| 141 | + |
| 142 | +- const contentEncoding = response?.headers?.['content-encoding'] |
| 143 | ++ const contentEncoding = typeof response?.headers?.['content-encoding'] === 'string' |
| 144 | ++ ? response.headers['content-encoding'] as string |
| 145 | ++ : undefined |
| 146 | + console.log('[Qwen] Content-Encoding:', contentEncoding) |
| 147 | + |
| 148 | + let buffer = '' |
| 149 | +@@ -899,7 +887,8 @@ export class QwenStreamHandler { |
| 150 | + |
| 151 | + let decompressStream: any = stream |
| 152 | + |
| 153 | +- const contentEncoding = response?.headers?.['content-encoding']?.toLowerCase() |
| 154 | ++ const rawEncoding = response?.headers?.['content-encoding'] |
| 155 | ++ const contentEncoding = typeof rawEncoding === 'string' ? rawEncoding.toLowerCase() : undefined |
| 156 | + if (contentEncoding === 'gzip') { |
| 157 | + console.log('[Qwen] Decompressing gzip stream...') |
| 158 | + decompressStream = stream.pipe(createGunzip()) |
| 159 | +diff --git a/src/main/proxy/adapters/zai.ts b/src/main/proxy/adapters/zai.ts |
| 160 | +index 967b77b..d8e91fc 100644 |
| 161 | +--- a/src/main/proxy/adapters/zai.ts |
| 162 | ++++ b/src/main/proxy/adapters/zai.ts |
| 163 | +@@ -107,6 +107,8 @@ function uuid(separator: boolean = true): string { |
| 164 | + } |
| 165 | + |
| 166 | + export class ZaiAdapter { |
| 167 | ++ private static chatIdCache: Map<string, string> = new Map() |
| 168 | ++ |
| 169 | + private provider: Provider |
| 170 | + private account: Account |
| 171 | + private token: string | null = null |
| 172 | +@@ -391,12 +393,41 @@ export class ZaiAdapter { |
| 173 | + |
| 174 | + const signaturePrompt = this.extractLastUserMessage(processedMessages) |
| 175 | + |
| 176 | +- // Always create a new chat (single-turn mode only) |
| 177 | +- const chatResult = await this.createChat(mappedModel, signaturePrompt) |
| 178 | +- const chatId = chatResult.chatId |
| 179 | +- const messageId = chatResult.messageId |
| 180 | +- const parentMessageId = null |
| 181 | +- console.log('[Z.ai] Created new chat:', chatId) |
| 182 | ++ // Reuse chat ID across requests for the same account when possible |
| 183 | ++ let chatId: string |
| 184 | ++ let messageId: string |
| 185 | ++ let parentMessageId: string | null = null |
| 186 | ++ |
| 187 | ++ // If client provides an existing chat ID, use it (multi-turn support) |
| 188 | ++ if (request.chatId) { |
| 189 | ++ chatId = request.chatId |
| 190 | ++ // For existing chat, we still need to generate a new message ID. |
| 191 | ++ // The API expects a new message ID for each turn, and optionally a parent message ID. |
| 192 | ++ messageId = uuid() |
| 193 | ++ // We don't know the parent message ID for existing chat, but we can set it to null. |
| 194 | ++ parentMessageId = null |
| 195 | ++ console.log('[Z.ai] Using existing chat ID from request:', chatId) |
| 196 | ++ } else { |
| 197 | ++ // Check cache for this account |
| 198 | ++ const cachedChatId = ZaiAdapter.chatIdCache.get(this.account.id) |
| 199 | ++ if (cachedChatId) { |
| 200 | ++ chatId = cachedChatId |
| 201 | ++ messageId = uuid() |
| 202 | ++ parentMessageId = null |
| 203 | ++ console.log('[Z.ai] Reusing cached chat ID for account:', chatId) |
| 204 | ++ } else { |
| 205 | ++ // Create a new chat |
| 206 | ++ const chatResult = await this.createChat(mappedModel, signaturePrompt) |
| 207 | ++ chatId = chatResult.chatId |
| 208 | ++ messageId = chatResult.messageId |
| 209 | ++ // Cache it for future requests from this account |
| 210 | ++ ZaiAdapter.chatIdCache.set(this.account.id, chatId) |
| 211 | ++ console.log('[Z.ai] Created new chat and cached:', chatId) |
| 212 | ++ } |
| 213 | ++ } |
| 214 | ++ |
| 215 | ++ // Note: The processedMessages array already contains the full conversation history, |
| 216 | ++ // so multi-turn context is preserved even when reusing the same chat_id. |
| 217 | + |
| 218 | + const requestId = uuid() |
| 219 | + const timestamp = Date.now() |
| 220 | +diff --git a/src/main/proxy/utils/messageToPrompt.ts b/src/main/proxy/utils/messageToPrompt.ts |
| 221 | +new file mode 100644 |
| 222 | +index 0000000..e60ba7c |
| 223 | +--- /dev/null |
| 224 | ++++ b/src/main/proxy/utils/messageToPrompt.ts |
| 225 | +@@ -0,0 +1,113 @@ |
| 226 | ++/** |
| 227 | ++ * Utility to convert an array of chat messages into a single prompt string |
| 228 | ++ * that can be sent to any API, preserving full conversation context. |
| 229 | ++ * |
| 230 | ++ * This approach ensures multi-turn conversation works across all providers |
| 231 | ++ * without relying on provider-specific session management. |
| 232 | ++ */ |
| 233 | ++ |
| 234 | ++import type { ChatMessage } from '../types' |
| 235 | ++ |
| 236 | ++export interface MessageContent { |
| 237 | ++ type: string |
| 238 | ++ text?: string |
| 239 | ++} |
| 240 | ++ |
| 241 | ++/** |
| 242 | ++ * Extract plain text content from a message, regardless of format |
| 243 | ++ */ |
| 244 | ++export function extractTextContent(content: string | MessageContent[] | null | undefined): string { |
| 245 | ++ if (!content) return '' |
| 246 | ++ if (typeof content === 'string') return content |
| 247 | ++ if (Array.isArray(content)) { |
| 248 | ++ return content |
| 249 | ++ .filter(part => part.type === 'text' && part.text) |
| 250 | ++ .map(part => part.text) |
| 251 | ++ .join(' ') |
| 252 | ++ } |
| 253 | ++ return '' |
| 254 | ++} |
| 255 | ++ |
| 256 | ++/** |
| 257 | ++ * Convert an array of messages into a single prompt string. |
| 258 | ++ * Format: |
| 259 | ++ * System: <system message> |
| 260 | ++ * |
| 261 | ++ * User: <user message> |
| 262 | ++ * Assistant: <assistant response> |
| 263 | ++ * User: <next user message> |
| 264 | ++ * ... |
| 265 | ++ */ |
| 266 | ++export function messagesToPrompt(messages: ChatMessage[]): string { |
| 267 | ++ const parts: string[] = [] |
| 268 | ++ |
| 269 | ++ for (const msg of messages) { |
| 270 | ++ const content = extractTextContent(msg.content) |
| 271 | ++ if (!content) continue |
| 272 | ++ |
| 273 | ++ switch (msg.role) { |
| 274 | ++ case 'system': |
| 275 | ++ parts.push(`System: ${content}`) |
| 276 | ++ break |
| 277 | ++ case 'user': |
| 278 | ++ parts.push(`User: ${content}`) |
| 279 | ++ break |
| 280 | ++ case 'assistant': |
| 281 | ++ parts.push(`Assistant: ${content}`) |
| 282 | ++ break |
| 283 | ++ case 'tool': |
| 284 | ++ // Tool responses are typically handled separately; include as user context |
| 285 | ++ parts.push(`Tool result: ${content}`) |
| 286 | ++ break |
| 287 | ++ default: |
| 288 | ++ // Unknown role, treat as user message to be safe |
| 289 | ++ parts.push(`User: ${content}`) |
| 290 | ++ break |
| 291 | ++ } |
| 292 | ++ } |
| 293 | ++ |
| 294 | ++ // Join with double newline for clear separation |
| 295 | ++ return parts.join('\n\n') |
| 296 | ++} |
| 297 | ++ |
| 298 | ++/** |
| 299 | ++ * Convert messages to prompt but preserve a system message separately |
| 300 | ++ * for providers that have a dedicated system prompt field. |
| 301 | ++ * Returns { systemPrompt: string, userPrompt: string } |
| 302 | ++ */ |
| 303 | ++export function splitSystemAndUserMessages(messages: ChatMessage[]): { |
| 304 | ++ systemPrompt: string |
| 305 | ++ userPrompt: string |
| 306 | ++} { |
| 307 | ++ let systemPrompt = '' |
| 308 | ++ const userParts: string[] = [] |
| 309 | ++ |
| 310 | ++ for (const msg of messages) { |
| 311 | ++ const content = extractTextContent(msg.content) |
| 312 | ++ if (!content) continue |
| 313 | ++ |
| 314 | ++ if (msg.role === 'system') { |
| 315 | ++ systemPrompt = systemPrompt ? `${systemPrompt}\n\n${content}` : content |
| 316 | ++ } else if (msg.role === 'user') { |
| 317 | ++ userParts.push(`User: ${content}`) |
| 318 | ++ } else if (msg.role === 'assistant') { |
| 319 | ++ userParts.push(`Assistant: ${content}`) |
| 320 | ++ } else if (msg.role === 'tool') { |
| 321 | ++ userParts.push(`Tool result: ${content}`) |
| 322 | ++ } else { |
| 323 | ++ userParts.push(`User: ${content}`) |
| 324 | ++ } |
| 325 | ++ } |
| 326 | ++ |
| 327 | ++ return { |
| 328 | ++ systemPrompt, |
| 329 | ++ userPrompt: userParts.join('\n\n'), |
| 330 | ++ } |
| 331 | ++} |
| 332 | ++ |
| 333 | ++/** |
| 334 | ++ * Estimate token count for a prompt string (rough approximation: 3 chars per token) |
| 335 | ++ */ |
| 336 | ++export function estimatePromptTokens(prompt: string): number { |
| 337 | ++ return Math.ceil(prompt.length / 3) |
| 338 | ++} |
0 commit comments