153 lines
3.5 KiB
JavaScript
153 lines
3.5 KiB
JavaScript
// src/controllers/chat.controller.js
|
|
// 채팅 컨트롤러 (OpenAI 호환 API)
|
|
|
|
const llmService = require('../services/llm.service');
|
|
const logger = require('../config/logger.config');
|
|
|
|
/**
|
|
* 채팅 완성 API (OpenAI 호환)
|
|
* POST /api/v1/chat/completions
|
|
*/
|
|
exports.completions = async (req, res, next) => {
|
|
try {
|
|
const {
|
|
model = 'gemini-2.0-flash',
|
|
messages,
|
|
temperature = 0.7,
|
|
max_tokens = 4096,
|
|
stream = false,
|
|
} = req.body;
|
|
|
|
const startTime = Date.now();
|
|
|
|
// 스트리밍 응답 처리
|
|
if (stream) {
|
|
return handleStreamingResponse(req, res, {
|
|
model,
|
|
messages,
|
|
temperature,
|
|
maxTokens: max_tokens,
|
|
});
|
|
}
|
|
|
|
// 일반 응답 처리
|
|
const result = await llmService.chat({
|
|
model,
|
|
messages,
|
|
temperature,
|
|
maxTokens: max_tokens,
|
|
userId: req.user.id,
|
|
apiKeyId: req.apiKey?.id,
|
|
});
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
|
|
// 사용량 정보 저장 (미들웨어에서 처리)
|
|
req.usageData = {
|
|
providerId: result.providerId,
|
|
providerName: result.provider,
|
|
modelName: result.model,
|
|
promptTokens: result.usage.promptTokens,
|
|
completionTokens: result.usage.completionTokens,
|
|
totalTokens: result.usage.totalTokens,
|
|
costUsd: result.cost,
|
|
responseTimeMs: responseTime,
|
|
success: true,
|
|
};
|
|
|
|
// OpenAI 호환 응답 형식
|
|
return res.json({
|
|
id: `chatcmpl-${Date.now()}`,
|
|
object: 'chat.completion',
|
|
created: Math.floor(Date.now() / 1000),
|
|
model: result.model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
message: {
|
|
role: 'assistant',
|
|
content: result.text,
|
|
},
|
|
finish_reason: 'stop',
|
|
},
|
|
],
|
|
usage: {
|
|
prompt_tokens: result.usage.promptTokens,
|
|
completion_tokens: result.usage.completionTokens,
|
|
total_tokens: result.usage.totalTokens,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error('채팅 완성 오류:', error);
|
|
|
|
// 사용량 정보 저장 (실패)
|
|
req.usageData = {
|
|
success: false,
|
|
errorMessage: error.message,
|
|
};
|
|
|
|
return next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* 스트리밍 응답 처리
|
|
*/
|
|
async function handleStreamingResponse(req, res, params) {
|
|
const { model, messages, temperature, maxTokens } = params;
|
|
|
|
// SSE 헤더 설정
|
|
res.setHeader('Content-Type', 'text/event-stream');
|
|
res.setHeader('Cache-Control', 'no-cache');
|
|
res.setHeader('Connection', 'keep-alive');
|
|
|
|
try {
|
|
// 스트리밍 응답 생성
|
|
const stream = await llmService.chatStream({
|
|
model,
|
|
messages,
|
|
temperature,
|
|
maxTokens,
|
|
userId: req.user.id,
|
|
apiKeyId: req.apiKey?.id,
|
|
});
|
|
|
|
// 스트림 이벤트 처리
|
|
for await (const chunk of stream) {
|
|
const data = {
|
|
id: `chatcmpl-${Date.now()}`,
|
|
object: 'chat.completion.chunk',
|
|
created: Math.floor(Date.now() / 1000),
|
|
model,
|
|
choices: [
|
|
{
|
|
index: 0,
|
|
delta: {
|
|
content: chunk.text,
|
|
},
|
|
finish_reason: chunk.done ? 'stop' : null,
|
|
},
|
|
],
|
|
};
|
|
|
|
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
|
}
|
|
|
|
// 스트림 종료
|
|
res.write('data: [DONE]\n\n');
|
|
res.end();
|
|
} catch (error) {
|
|
logger.error('스트리밍 오류:', error);
|
|
|
|
const errorData = {
|
|
error: {
|
|
message: error.message,
|
|
type: 'server_error',
|
|
},
|
|
};
|
|
|
|
res.write(`data: ${JSON.stringify(errorData)}\n\n`);
|
|
res.end();
|
|
}
|
|
}
|