234 lines
5.7 KiB
TypeScript
234 lines
5.7 KiB
TypeScript
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<string> {
|
|
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<string> {
|
|
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<boolean> {
|
|
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,
|
|
};
|
|
}
|
|
|