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

/**
* 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 };