/** * Mock LLM Server — E2E 冒烟测试的本地 OpenAI 兼容 Provider(v0.8.1 P2-5) * * 实现 DeepSeek 适配器实际消费的最小协议面: * - POST {baseURL}/chat/completions(stream=true):按 SSE 推送一段固定文本 + * finish_reason=stop + usage + [DONE]; * - POST /chat/completions(stream=false):非流式 JSON(压缩摘要等内部调用兜底)。 * * 端口随机分配(127.0.0.1),测试结束关闭 —— 不触外网、不落真实会话数据。 */ import { createServer, type Server } from 'http'; import type { AddressInfo } from 'net'; /** 流式回复的固定文本(断言锚点) */ export const MOCK_REPLY = 'Hello from mock LLM. E2E smoke reply.'; export interface MockLLMHandle { url: string; close: () => Promise; /** 收到的 chat/completions 请求体列表(断言用) */ readonly requests: Array>; } export function startMockLLM(): Promise { const requests: Array> = []; const server: Server = createServer((req, res) => { if (!req.url?.includes('/chat/completions')) { res.writeHead(404).end(); return; } let body = ''; req.on('data', (chunk) => { body += chunk; }); req.on('end', () => { let parsed: Record = {}; try { parsed = JSON.parse(body) as Record; } catch { /* 忽略解析失败 */ } requests.push(parsed); const isStream = parsed.stream === true; if (!isStream) { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end( JSON.stringify({ id: 'mock-1', model: parsed.model ?? 'mock', choices: [ { index: 0, message: { role: 'assistant', content: MOCK_REPLY }, finish_reason: 'stop', }, ], usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 }, }), ); return; } res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive', }); const frame = (payload: Record): void => { res.write(`data: ${JSON.stringify(payload)}\n\n`); }; // 首 chunk:正文增量 frame({ id: 'mock-1', model: parsed.model ?? 'mock', choices: [{ index: 0, delta: { role: 'assistant', content: MOCK_REPLY } }], }); // 末帧:finish_reason + usage frame({ id: 'mock-1', model: parsed.model ?? 'mock', choices: [{ index: 0, delta: {}, finish_reason: 'stop' }], usage: { prompt_tokens: 10, completion_tokens: 8, total_tokens: 18 }, }); res.write('data: [DONE]\n\n'); res.end(); }); }); return new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { const addr = server.address() as AddressInfo; resolve({ url: `http://127.0.0.1:${addr.port}`, close: () => new Promise((r) => server.close(() => r())), requests, }); }); }); }