214 lines
6.5 KiB
JavaScript
214 lines
6.5 KiB
JavaScript
import { NextResponse } from 'next/server';
|
|
import { db } from '@/lib/db/index';
|
|
import LLMClient from '@/lib/llm/core/index';
|
|
import { getModelConfigById } from '@/lib/db/model-config';
|
|
|
|
/**
|
|
* Stream answers from two models for the current question
|
|
*/
|
|
export async function GET(request, { params }) {
|
|
const { projectId, taskId } = params;
|
|
|
|
try {
|
|
if (!projectId || !taskId) {
|
|
return NextResponse.json({ error: 'Missing required parameters' }, { status: 400 });
|
|
}
|
|
|
|
// Fetch task
|
|
const task = await db.task.findUnique({
|
|
where: { id: taskId }
|
|
});
|
|
|
|
if (!task || task.taskType !== 'blind-test') {
|
|
return NextResponse.json({ error: 'Task not found' }, { status: 404 });
|
|
}
|
|
|
|
// Parse task detail
|
|
const detail = JSON.parse(task.detail || '{}');
|
|
const modelInfo = JSON.parse(task.modelInfo || '{}');
|
|
const { questionIds = [], currentIndex = 0 } = detail;
|
|
|
|
// Check if task is completed
|
|
if (currentIndex >= questionIds.length) {
|
|
return NextResponse.json({ completed: true });
|
|
}
|
|
|
|
// Fetch current question
|
|
const currentQuestionId = questionIds[currentIndex];
|
|
const currentQuestion = await db.evalDatasets.findUnique({
|
|
where: { id: currentQuestionId }
|
|
});
|
|
|
|
if (!currentQuestion) {
|
|
return NextResponse.json({ error: 'Question not found' }, { status: 404 });
|
|
}
|
|
|
|
// Fetch model configs
|
|
const [modelConfigA, modelConfigB] = await Promise.all([
|
|
getModelConfigById(modelInfo.modelA.providerId),
|
|
getModelConfigById(modelInfo.modelB.providerId)
|
|
]);
|
|
|
|
if (!modelConfigA || !modelConfigB) {
|
|
return NextResponse.json({ error: 'Model configuration not found' }, { status: 400 });
|
|
}
|
|
|
|
// Randomly swap positions (core blind-test behavior)
|
|
const isSwapped = Math.random() > 0.5;
|
|
|
|
// Create streaming response
|
|
const encoder = new TextEncoder();
|
|
const stream = new ReadableStream({
|
|
async start(controller) {
|
|
try {
|
|
// Send init message
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'init',
|
|
question: currentQuestion.question,
|
|
questionId: currentQuestion.id,
|
|
questionIndex: currentIndex + 1,
|
|
totalQuestions: questionIds.length,
|
|
isSwapped
|
|
}) + '\n'
|
|
)
|
|
);
|
|
|
|
// Prepare messages
|
|
const messages = [
|
|
{
|
|
role: 'system',
|
|
content: "You are a helpful assistant. Provide detailed and accurate answers to the user's question."
|
|
},
|
|
{ role: 'user', content: currentQuestion.question }
|
|
];
|
|
|
|
// Create LLM clients
|
|
const clientA = new LLMClient({
|
|
projectId,
|
|
...modelConfigA
|
|
});
|
|
|
|
const clientB = new LLMClient({
|
|
projectId,
|
|
...modelConfigB
|
|
});
|
|
|
|
let answerA = '';
|
|
let answerB = '';
|
|
const startTime = Date.now();
|
|
|
|
// Call both models in parallel (streaming)
|
|
await Promise.all([
|
|
(async () => {
|
|
try {
|
|
const response = await clientA.chatStreamAPI(messages);
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
const chunk = decoder.decode(value, { stream: true });
|
|
answerA += chunk;
|
|
|
|
// Send chunk update
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'chunk',
|
|
model: isSwapped ? 'B' : 'A',
|
|
content: chunk
|
|
}) + '\n'
|
|
)
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error('Model A call failed:', err);
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'error',
|
|
model: isSwapped ? 'B' : 'A',
|
|
error: err.message
|
|
}) + '\n'
|
|
)
|
|
);
|
|
}
|
|
})(),
|
|
(async () => {
|
|
try {
|
|
const response = await clientB.chatStreamAPI(messages);
|
|
const reader = response.body.getReader();
|
|
const decoder = new TextDecoder();
|
|
|
|
while (true) {
|
|
const { done, value } = await reader.read();
|
|
if (done) break;
|
|
|
|
const chunk = decoder.decode(value, { stream: true });
|
|
answerB += chunk;
|
|
|
|
// Send chunk update
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'chunk',
|
|
model: isSwapped ? 'A' : 'B',
|
|
content: chunk
|
|
}) + '\n'
|
|
)
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error('Model B call failed:', err);
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'error',
|
|
model: isSwapped ? 'A' : 'B',
|
|
error: err.message
|
|
}) + '\n'
|
|
)
|
|
);
|
|
}
|
|
})()
|
|
]);
|
|
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Send done message
|
|
controller.enqueue(
|
|
encoder.encode(
|
|
JSON.stringify({
|
|
type: 'done',
|
|
duration,
|
|
answerA: isSwapped ? answerB : answerA,
|
|
answerB: isSwapped ? answerA : answerB
|
|
}) + '\n'
|
|
)
|
|
);
|
|
|
|
controller.close();
|
|
} catch (error) {
|
|
console.error('Streaming handler failed:', error);
|
|
controller.error(error);
|
|
}
|
|
}
|
|
});
|
|
|
|
return new Response(stream, {
|
|
headers: {
|
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
'Cache-Control': 'no-cache',
|
|
Connection: 'keep-alive'
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error('API error:', error);
|
|
return NextResponse.json({ error: error.message }, { status: 500 });
|
|
}
|
|
}
|