/** * 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 { 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 { return new Promise((resolve) => { if (server) { server.close(() => resolve()); } else { resolve(); } }); } export { start, stop };