routerService.test.ts 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. import { createServer, type Server } from 'node:http';
  2. import { afterAll, beforeAll, afterEach, describe, expect, it } from 'vitest';
  3. import { ChatRouterService } from './routerService';
  4. let lastUpstreamBody: string | null = null;
  5. let lastUpstreamHeaders: Record<string, string | string[] | undefined> = {};
  6. let upstreamAbortCount = 0;
  7. /** 上游行为:'sse' 正常流 | 'slow' 慢 prefill | 'error' 500 */
  8. let upstreamMode = 'sse';
  9. const upstream: Server = createServer((req, res) => {
  10. const chunks: Buffer[] = [];
  11. req.on('data', (c: Buffer) => chunks.push(c));
  12. req.on('end', () => {
  13. lastUpstreamBody = Buffer.concat(chunks).toString('utf8');
  14. lastUpstreamHeaders = req.headers;
  15. if (upstreamMode === 'error') {
  16. res.writeHead(500, { 'content-type': 'application/json' });
  17. res.end(JSON.stringify({ error: { message: 'boom' } }));
  18. return;
  19. }
  20. // 非流式请求(防御路径):回 JSON chat completion;流式回 SSE
  21. const wantsStream = (() => {
  22. try { return (JSON.parse(lastUpstreamBody ?? '{}') as { stream?: boolean }).stream === true; } catch { return true; }
  23. })();
  24. if (!wantsStream) {
  25. res.writeHead(200, { 'content-type': 'application/json' });
  26. res.end(JSON.stringify({
  27. choices: [{ message: { content: '你好世界' }, finish_reason: 'stop' }],
  28. usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 },
  29. }));
  30. return;
  31. }
  32. res.writeHead(200, { 'content-type': 'text/event-stream' });
  33. const finish = (): void => {
  34. res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: '世界' } }, { finish_reason: 'stop' }] })}\n\n`);
  35. res.write(`data: ${JSON.stringify({ choices: [], usage: { prompt_tokens: 5, completion_tokens: 2, total_tokens: 7 } })}\n\n`);
  36. res.write('data: [DONE]\n\n');
  37. res.end();
  38. };
  39. if (upstreamMode === 'slow') {
  40. // 慢 prefill:3 秒后才发首字节,路由层必须在这之前就把 SSE 头与 begin 事件发出去
  41. setTimeout(() => {
  42. res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: '晚到' } }] })}\n\n`);
  43. finish();
  44. }, 3_000);
  45. } else {
  46. res.write(`data: ${JSON.stringify({ choices: [{ delta: { content: '你好' } }] })}\n\n`);
  47. finish();
  48. }
  49. });
  50. req.on('close', () => {
  51. if (!res.writableEnded) upstreamAbortCount += 1;
  52. });
  53. });
  54. function parseSse(raw: string): Array<{ event: string; data: Record<string, unknown> }> {
  55. return raw.split('\n\n').filter((part) => part.includes('event:')).map((part) => {
  56. const event = /^event: (.*)$/mu.exec(part)?.[1] ?? '';
  57. const data = /^data: (.*)$/mu.exec(part)?.[1] ?? '{}';
  58. return { event, data: JSON.parse(data) as Record<string, unknown> };
  59. });
  60. }
  61. let upstreamUrl = '';
  62. const routers: ChatRouterService[] = [];
  63. beforeAll(async () => {
  64. await new Promise<void>((resolve) => {
  65. upstream.listen(0, '127.0.0.1', () => resolve());
  66. });
  67. const address = upstream.address();
  68. upstreamUrl = `http://127.0.0.1:${(address as { port: number }).port}/v1`;
  69. });
  70. async function startRouter(mode: string): Promise<ChatRouterService> {
  71. upstreamMode = mode;
  72. const router = new ChatRouterService();
  73. routers.push(router);
  74. await router.start({
  75. upstreamBaseUrl: upstreamUrl,
  76. forwardHeaderNames: ['x-extra'],
  77. onDiagnostic: () => {},
  78. });
  79. return router;
  80. }
  81. afterEach(async () => {
  82. upstreamAbortCount = 0;
  83. for (const router of routers.splice(0)) await router.stop();
  84. });
  85. afterAll(() => {
  86. upstream.close();
  87. });
  88. describe('ChatRouterService', () => {
  89. it('非流式:上游收到 /chat/completions 与透传头,下游拿到 Responses JSON', async () => {
  90. const router = await startRouter('sse');
  91. // router.url 本身以 /v1 结尾(codex 语义),所以请求路径拼 /responses
  92. const res = await fetch(`${router.url}/responses`, {
  93. method: 'POST',
  94. headers: { 'content-type': 'application/json', authorization: 'Bearer sk-test', 'x-extra': 'v1' },
  95. body: JSON.stringify({ model: 'qwen', instructions: '你是助手', input: '你好', stream: false }),
  96. });
  97. expect(res.status).toBe(200);
  98. const payload = await res.json() as Record<string, unknown>;
  99. expect(payload.object).toBe('response');
  100. expect(payload.status).toBe('completed');
  101. expect((payload.usage as Record<string, unknown>).total_tokens).toBe(7);
  102. expect(lastUpstreamBody).not.toBeNull();
  103. const chatRequest = JSON.parse(lastUpstreamBody!) as Record<string, unknown>;
  104. expect(chatRequest.stream).toBe(false);
  105. expect(chatRequest.stream_options).toBeUndefined();
  106. expect(chatRequest.messages).toEqual([
  107. { role: 'developer', content: '你是助手' },
  108. { role: 'user', content: '你好' },
  109. ]);
  110. expect(lastUpstreamHeaders.authorization).toBe('Bearer sk-test');
  111. expect(lastUpstreamHeaders['x-extra']).toBe('v1');
  112. });
  113. it('流式:先 created 后 completed,delta 拼接正确,上游 body 带 include_usage', async () => {
  114. const router = await startRouter('sse');
  115. const res = await fetch(`${router.url}/responses`, {
  116. method: 'POST',
  117. headers: { 'content-type': 'application/json', authorization: 'Bearer sk-stream' },
  118. body: JSON.stringify({ model: 'qwen', input: '你好', stream: true }),
  119. });
  120. expect(res.status).toBe(200);
  121. expect(res.headers.get('content-type')).toContain('text/event-stream');
  122. const raw = await res.text();
  123. const events = parseSse(raw);
  124. expect(events[0].event).toBe('response.created');
  125. expect(events.at(-1)?.event).toBe('response.completed');
  126. const text = events.filter((e) => e.event === 'response.output_text.delta')
  127. .map((e) => e.data.delta as string).join('');
  128. expect(text).toBe('你好世界');
  129. const completed = events.at(-1)!.data.response as Record<string, unknown>;
  130. expect(completed.id).toMatch(/^resp_/);
  131. expect(completed.usage).toMatchObject({ input_tokens: 5, output_tokens: 2, total_tokens: 7 });
  132. expect(lastUpstreamBody).not.toBeNull();
  133. const chatRequest = JSON.parse(lastUpstreamBody!) as Record<string, unknown>;
  134. expect(chatRequest.stream).toBe(true);
  135. expect(chatRequest.stream_options).toEqual({ include_usage: true });
  136. expect(lastUpstreamHeaders.authorization).toBe('Bearer sk-stream');
  137. });
  138. it('上游 500(流式):以 response.failed 收尾,codex 走错误映射', async () => {
  139. const router = await startRouter('error');
  140. const res = await fetch(`${router.url}/responses`, {
  141. method: 'POST',
  142. headers: { 'content-type': 'application/json' },
  143. body: JSON.stringify({ model: 'qwen', input: 'hi', stream: true }),
  144. });
  145. expect(res.status).toBe(200);
  146. const events = parseSse(await res.text());
  147. expect(events.at(-1)?.event).toBe('response.failed');
  148. expect((events.at(-1)!.data.response as Record<string, unknown>).error).toMatchObject({ code: 'upstream_error' });
  149. });
  150. it('只接受 POST /v1/responses;stop 后端口释放', async () => {
  151. const router = await startRouter('sse');
  152. const getUrl = `${router.url}/models`;
  153. const wrong = await fetch(getUrl);
  154. expect(wrong.status).toBe(404);
  155. const url = router.url;
  156. await router.stop();
  157. await expect(fetch(url!)).rejects.toThrow();
  158. });
  159. it('客户端提前挂断:1 秒内取消上游请求', async () => {
  160. const router = await startRouter('slow');
  161. const controller = new AbortController();
  162. const res = await fetch(`${router.url}/responses`, {
  163. method: 'POST',
  164. headers: { 'content-type': 'application/json' },
  165. body: JSON.stringify({ model: 'qwen', input: 'hi', stream: true }),
  166. signal: controller.signal,
  167. });
  168. await res.arrayBuffer().catch(() => undefined);
  169. controller.abort();
  170. await new Promise((resolve) => setTimeout(resolve, 1_000));
  171. expect(upstreamAbortCount).toBeGreaterThanOrEqual(1);
  172. }, 10_000);
  173. it('慢 prefill:上游 3 秒不响应,下游 1.5 秒内已收到 begin 事件不掐线', async () => {
  174. const router = await startRouter('slow');
  175. const startedAt = Date.now();
  176. const res = await fetch(`${router.url}/responses`, {
  177. method: 'POST',
  178. headers: { 'content-type': 'application/json' },
  179. body: JSON.stringify({ model: 'qwen', input: 'hi', stream: true }),
  180. });
  181. const reader = res.body!.getReader();
  182. const decoder = new TextDecoder();
  183. let firstFrameAt = -1;
  184. let raw = '';
  185. for (;;) {
  186. const { done, value } = await reader.read();
  187. if (done) break;
  188. raw += decoder.decode(value, { stream: true });
  189. if (firstFrameAt < 0 && raw.includes('response.created')) {
  190. firstFrameAt = Date.now() - startedAt;
  191. break; // 拿到 begin 即验证完毕,不等慢 prefill
  192. }
  193. }
  194. expect(firstFrameAt).toBeGreaterThanOrEqual(0);
  195. expect(firstFrameAt).toBeLessThan(1_500);
  196. reader.cancel().catch(() => undefined);
  197. }, 15_000);
  198. });