// 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(); } }