g2api / test /unit-openai-stream-usage.mjs
LerinaOwO's picture
Upload 98 files
097fb32 verified
/**
* 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);