import "server-only"; /** * 本地千问模型 Provider * 支持千问2.5-VL-3B-Instruct 本地部署模型 */ interface LocalQwenConfig { baseURL: string; apiKey: string; timeout?: number; } interface QwenMessage { role: string; content: Array<{ type: string; text?: string; image?: string }>; } interface QwenChatRequest { messages: QwenMessage[]; max_new_tokens?: number; temperature?: number; top_p?: number; do_sample?: boolean; } interface QwenChatResponse { response: string; usage: { tokens: number; }; processing_time: number; } /** * 创建本地千问模型客户端 */ export function createLocalQwen(config: LocalQwenConfig) { const { baseURL, apiKey, timeout = 30000 } = config; // 使用 fetch API 的辅助函数 async function fetchAPI(endpoint: string, options: RequestInit = {}) { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(`${baseURL}${endpoint}`, { ...options, signal: controller.signal, headers: { "X-API-Key": apiKey, "Content-Type": "application/json", ...options.headers, }, }); clearTimeout(timeoutId); if (!response.ok) { const error = await response.json().catch(() => ({ detail: response.statusText })); throw new Error(error.detail || `HTTP ${response.status}`); } return await response.json(); } finally { clearTimeout(timeoutId); } } /** * 转换 AI SDK 消息格式到千问格式 */ function convertMessages(messages: any[]): QwenMessage[] { return messages.map((msg) => { // 如果消息是字符串,转换为标准格式 if (typeof msg.content === "string") { return { role: msg.role, content: [ { type: "text", text: msg.content, }, ], }; } // 如果消息已经是数组格式,直接返回 if (Array.isArray(msg.content)) { return { role: msg.role, content: msg.content, }; } // 默认转换 return { role: msg.role, content: [ { type: "text", text: String(msg.content), }, ], }; }); } /** * 生成聊天响应 */ async function chat( messages: any[], options: { maxTokens?: number; temperature?: number; topP?: number; } = {} ): Promise { try { console.log("[LocalQwen] Sending chat request to:", baseURL); console.log("[LocalQwen] Messages count:", messages.length); const requestBody: QwenChatRequest = { messages: convertMessages(messages), max_new_tokens: options.maxTokens || 512, temperature: options.temperature || 0.7, top_p: options.topP || 0.9, do_sample: true, }; const response = await fetchAPI("/chat/text", { method: "POST", body: JSON.stringify(requestBody), }); console.log("[LocalQwen] Response received:", { tokens: response.usage.tokens, processingTime: response.processing_time, }); return response.response; } catch (error: unknown) { console.error("[LocalQwen] Chat error:", error); const errorMsg = error instanceof Error ? error.message : String(error); throw new Error(`本地千问模型请求失败: ${errorMsg}`); } } /** * 创建流式响应(本地模型暂不支持流式,模拟流式效果) */ async function* chatStream( messages: any[], options: { maxTokens?: number; temperature?: number; topP?: number; } = {} ): AsyncGenerator { const response = await chat(messages, options); // 模拟流式输出,每次输出几个字符 const chunkSize = 5; for (let i = 0; i < response.length; i += chunkSize) { yield response.slice(i, i + chunkSize); // 添加小延迟使流式效果更自然 await new Promise((resolve) => setTimeout(resolve, 50)); } } /** * 健康检查 */ async function healthCheck(): Promise { try { const response = await fetchAPI("/health"); return response.status === "healthy"; } catch (error: unknown) { console.error("[LocalQwen] Health check failed:", error); return false; } } // 返回类似 AI SDK LanguageModel 的接口 return { chat: (modelId: string) => ({ doGenerate: async (options: any) => { const response = await chat(options.prompt, { maxTokens: options.maxTokens, temperature: options.temperature, topP: options.topP, }); return { text: response, finishReason: "stop" as const, usage: { promptTokens: 0, completionTokens: 0, }, }; }, doStream: async function* (options: any) { for await (const chunk of chatStream(options.prompt, { maxTokens: options.maxTokens, temperature: options.temperature, topP: options.topP, })) { yield { type: "text-delta" as const, textDelta: chunk, }; } yield { type: "finish" as const, finishReason: "stop" as const, usage: { promptTokens: 0, completionTokens: 0, }, }; }, modelId, provider: "local-qwen", specificationVersion: "v1" as const, defaultObjectGenerationMode: "json" as const, }), healthCheck, }; }