You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
125 lines
3.2 KiB
125 lines
3.2 KiB
/**
|
|
* Mock LLM 服务器 — OpenAI 兼容格式,用于 E2E 测试。
|
|
*
|
|
* 启动后监听 MOCK_LLM_PORT(默认 3600),提供:
|
|
* - POST /v1/chat/completions 流式聊天补全(SSE)
|
|
*
|
|
* 响应内容固定为 "你好!这是一个测试回复。" 分段流式输出,
|
|
* 保证 agent chat 的 streamText 能正常消费。
|
|
*/
|
|
import { createServer, type Server } from "node:http";
|
|
|
|
const PORT = Number(process.env.MOCK_LLM_PORT) || 3600;
|
|
const REPLY = "你好!这是一个测试回复。";
|
|
|
|
function splitChunks(text: string): string[] {
|
|
const chunks: string[] = [];
|
|
for (const ch of text) {
|
|
chunks.push(ch);
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function handleChatCompletions(req: any, res: any) {
|
|
let body = "";
|
|
req.on("data", (chunk: Buffer) => {
|
|
body += chunk.toString();
|
|
});
|
|
req.on("end", () => {
|
|
let parsed: any = {};
|
|
try {
|
|
parsed = JSON.parse(body);
|
|
} catch {
|
|
parsed = {};
|
|
}
|
|
|
|
const model = parsed.model || "mock-model";
|
|
const chunks = splitChunks(REPLY);
|
|
|
|
res.writeHead(200, {
|
|
"Content-Type": "text/event-stream",
|
|
"Cache-Control": "no-cache",
|
|
Connection: "keep-alive",
|
|
});
|
|
|
|
let idx = 0;
|
|
const timer = setInterval(() => {
|
|
if (idx < chunks.length) {
|
|
const delta = chunks[idx];
|
|
const sseData = JSON.stringify({
|
|
id: "chatcmpl-mock",
|
|
object: "chat.completion.chunk",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: { content: delta },
|
|
finish_reason: null,
|
|
},
|
|
],
|
|
});
|
|
res.write(`data: ${sseData}\n\n`);
|
|
idx++;
|
|
} else {
|
|
const doneData = JSON.stringify({
|
|
id: "chatcmpl-mock",
|
|
object: "chat.completion.chunk",
|
|
created: Math.floor(Date.now() / 1000),
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {},
|
|
finish_reason: "stop",
|
|
},
|
|
],
|
|
});
|
|
res.write(`data: ${doneData}\n\n`);
|
|
res.write("data: [DONE]\n\n");
|
|
res.end();
|
|
clearInterval(timer);
|
|
}
|
|
}, 20);
|
|
});
|
|
}
|
|
|
|
let server: Server;
|
|
|
|
function start(): Promise<Server> {
|
|
return new Promise((resolve) => {
|
|
server = createServer((req, res) => {
|
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
res.setHeader("Access-Control-Allow-Headers", "*");
|
|
res.setHeader("Access-Control-Allow-Methods", "*");
|
|
if (req.method === "OPTIONS") {
|
|
res.writeHead(204);
|
|
res.end();
|
|
return;
|
|
}
|
|
const url = req.url || "";
|
|
if (url.startsWith("/v1/chat/completions") && req.method === "POST") {
|
|
handleChatCompletions(req, res);
|
|
return;
|
|
}
|
|
res.writeHead(404, { "Content-Type": "application/json" });
|
|
res.end(JSON.stringify({ error: "not found" }));
|
|
});
|
|
server.listen(PORT, () => {
|
|
console.log(`[mock-llm] listening on http://localhost:${PORT}`);
|
|
resolve(server);
|
|
});
|
|
});
|
|
}
|
|
|
|
function stop(): Promise<void> {
|
|
return new Promise((resolve) => {
|
|
if (server) {
|
|
server.close(() => resolve());
|
|
} else {
|
|
resolve();
|
|
}
|
|
});
|
|
}
|
|
|
|
export { start, stop };
|
|
|