File size: 4,599 Bytes
097fb32 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | /**
* test/unit-openai-stream-usage.mjs
*
* 回归测试:/v1/chat/completions 流式最后一帧应携带 usage
* 运行方式:npm run build && node test/unit-openai-stream-usage.mjs
*/
import { handleOpenAIChatCompletions } from '../dist/openai-handler.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
Promise.resolve()
.then(fn)
.then(() => {
console.log(` ✅ ${name}`);
passed++;
})
.catch((e) => {
console.error(` ❌ ${name}`);
console.error(` ${e.message}`);
failed++;
});
}
function assert(condition, msg) {
if (!condition) throw new Error(msg || 'Assertion failed');
}
function assertEqual(a, b, msg) {
const as = JSON.stringify(a), bs = JSON.stringify(b);
if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`);
}
function createCursorSseResponse(deltas) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
for (const delta of deltas) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'text-delta', delta })}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
});
}
class MockResponse {
constructor() {
this.statusCode = 200;
this.headers = {};
this.body = '';
this.ended = false;
}
writeHead(statusCode, headers) {
this.statusCode = statusCode;
this.headers = { ...this.headers, ...headers };
}
write(chunk) {
this.body += String(chunk);
return true;
}
end(chunk = '') {
this.body += String(chunk);
this.ended = true;
}
}
function extractDataChunks(sseText) {
return sseText
.split('\n\n')
.map(part => part.trim())
.filter(Boolean)
.filter(part => part.startsWith('data: '))
.map(part => part.slice(6))
.filter(part => part !== '[DONE]')
.map(part => JSON.parse(part));
}
console.log('\n📦 [1] OpenAI Chat Completions 流式 usage 回归\n');
const pending = [];
pending.push((async () => {
const originalFetch = global.fetch;
try {
global.fetch = async () => createCursorSseResponse(['Hello', ' world from Cursor']);
const req = {
method: 'POST',
path: '/v1/chat/completions',
body: {
model: 'gpt-4.1',
stream: true,
messages: [
{ role: 'user', content: 'Write a short greeting in English.' },
],
},
};
const res = new MockResponse();
await handleOpenAIChatCompletions(req, res);
assertEqual(res.statusCode, 200, 'statusCode 应为 200');
assert(res.ended, '响应应结束');
const chunks = extractDataChunks(res.body);
assert(chunks.length >= 2, '至少应包含 role chunk 和完成 chunk');
const lastChunk = chunks[chunks.length - 1];
assertEqual(lastChunk.object, 'chat.completion.chunk');
assert(lastChunk.usage, '最后一帧应包含 usage');
assert(typeof lastChunk.usage.prompt_tokens === 'number' && lastChunk.usage.prompt_tokens > 0, 'prompt_tokens 应为正数');
assert(typeof lastChunk.usage.completion_tokens === 'number' && lastChunk.usage.completion_tokens > 0, 'completion_tokens 应为正数');
assertEqual(
lastChunk.usage.total_tokens,
lastChunk.usage.prompt_tokens + lastChunk.usage.completion_tokens,
'total_tokens 应等于 prompt_tokens + completion_tokens'
);
assertEqual(lastChunk.choices[0].finish_reason, 'stop', '最后一帧 finish_reason 应为 stop');
const contentChunks = chunks.filter(chunk => chunk.choices?.[0]?.delta?.content);
assert(contentChunks.length > 0, '应输出至少一个 content chunk');
} finally {
global.fetch = originalFetch;
}
})().then(() => {
console.log(' ✅ 流式最后一帧携带 usage');
passed++;
}).catch((e) => {
console.error(' ❌ 流式最后一帧携带 usage');
console.error(` ${e.message}`);
failed++;
}));
await Promise.all(pending);
console.log('\n' + '═'.repeat(55));
console.log(` 结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计`);
console.log('═'.repeat(55) + '\n');
if (failed > 0) process.exit(1);
|