// server.js - OpenAI to OpenCode Zen Proxy (Optimized for Render Free Tier) const express = require('express'); const cors = require('cors'); const axios = require('axios'); const path = require('path'); const http = require('http'); const https = require('https'); const compression = require('compression'); const rateLimit = require('express-rate-limit'); const app = express(); // ===== [QUAN TRỌNG CHO RENDER] ===== // Giúp Express nhận diện đúng IP thực của người dùng đứng sau Load Balancer của Render. // Nếu không có dòng này, toàn bộ user sẽ bị định diện chung 1 IP và khóa Rate Limit nhầm! app.set('trust proxy', 1); // ===== TỐI ƯU HIỆU NĂNG ===== // Nén gzipping cho toàn bộ response -> Giảm băng thông, AI trả lời cảm giác nhanh hơn app.use(compression()); // ===== CẤU HÌNH BẢO MẬT CORS ===== // Đọc danh sách domain hợp lệ từ biến môi trường ALLOWED_ORIGINS (ví dụ: https://myapp.com,http://localhost:3000) // Nếu không cấu hình ALLOWED_ORIGINS, mặc định mở cho tất cả (*) const allowedOrigins = process.env.ALLOWED_ORIGINS ? process.env.ALLOWED_ORIGINS.split(',') : '*'; app.use(cors({ origin: function (origin, callback) { if (!origin || allowedOrigins === '*' || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Blocked by CORS policy: Invalid Origin')); } }, methods: ['GET', 'POST', 'OPTIONS'], allowedHeaders: ['Content-Type', 'Authorization'] })); // ===== GIỚI HẠN PAYLOAD (BẢO VỆ RAM) ===== // Hạ từ 10MB xuống 1MB để tránh hacker/user gửi payload lớn làm sập RAM 512MB của Render app.use(express.json({ limit: '1mb' })); // ===== CẤU HÌNH TỪ ENVIRONMENT VARIABLES ===== const ZEN_API_BASE = process.env.ZEN_API_BASE || 'https://opencode.ai/zen/v1'; const ZEN_API_KEY = process.env.ZEN_API_KEY; const PORT = process.env.PORT || 3000; if (!ZEN_API_KEY) { console.warn('⚠️ WARNING: ZEN_API_KEY is not set!'); } // ===== TỐI ƯU KẾT NỐI MẠNG (HTTP KEEP-ALIVE) ===== // Giữ kết nối mở để tái sử dụng, tránh tốn CPU/RAM tạo lại kết nối SSL cho mỗi request const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 }); const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 }); const apiInstance = axios.create({ baseURL: ZEN_API_BASE, timeout: 45000, // Hạ từ 120s xuống 45s để nhanh chóng giải phóng RAM nếu server AI upstream bị treo httpAgent, httpsAgent, headers: { 'Authorization': `Bearer ${ZEN_API_KEY}`, 'Content-Type': 'application/json' }, validateStatus: status => status < 500 }); // ===== RATE LIMITER (20 requests/phút/IP) ===== const apiLimiter = rateLimit({ windowMs: 60 * 1000, // 1 phút max: 20, // 20 requests mỗi phút message: { error: { message: 'Too many requests. Please wait a moment and try again.', type: 'rate_limit_error', limit: 20, window: '1 minute' } }, standardHeaders: true, legacyHeaders: false, keyGenerator: (req) => { // Sẽ lấy chính xác IP client nhờ cấu hình app.set('trust proxy', 1) ở trên return req.ip || req.headers['x-forwarded-for'] || req.connection.remoteAddress; }, handler: (req, res) => { res.status(429).json({ error: { message: 'Rate limit exceeded. Maximum 20 requests per minute allowed.', type: 'rate_limit_error', retry_after: 60 } }); } }); // ===== MODEL MAPPING ===== const MODEL_MAPPING = { 'big-pickle': 'big-pickle', 'deepseek-v4-flash-free': 'deepseek-v4-flash-free', 'mimo-v2.5-free': 'mimo-v2.5-free', 'laguna-s-2.1-free': 'laguna-s-2.1-free', 'north-mini-code-free': 'north-mini-code-free', 'nemotron-3-ultra-free': 'nemotron-3-ultra-free' }; const MODEL_LIST = Object.keys(MODEL_MAPPING).map(model => ({ id: model, object: 'model', created: Math.floor(Date.now() / 1000), owned_by: 'opencode-free-proxy', permission: [] })); // ===== HÀM CHUẨN HÓA CONTENT ===== function extractContent(message) { if (!message) return null; if (Array.isArray(message.content)) { const textParts = message.content .filter(part => part && (part.type === 'text' || part.text)) .map(part => (typeof part.text === 'string' ? part.text : part)); if (textParts.length > 0) return textParts.join('\n').trim(); } const fields = [ message.content, message.reasoning_content, message.reasoning, message.text, message.response, message.output ]; for (const field of fields) { if (field && typeof field === 'string' && field.trim().length > 0) { return field.trim(); } } if (message.content && typeof message.content === 'object') { try { return JSON.stringify(message.content); } catch (e) { return null; } } return null; } // ===== HÀM CHUẨN HÓA RESPONSE ===== function normalizeResponse(data, originalModel) { const choice = data.choices?.[0] || {}; const message = choice.message || {}; let content = extractContent(message); if (!content && data.choices?.[0]?.delta) { content = extractContent(data.choices[0].delta); } if (!content) { content = "I'm here to help! What would you like to know?"; } return { id: data.id || `chatcmpl-${Date.now()}`, object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: originalModel || 'mimo-v2.5-free', choices: [{ index: 0, message: { role: 'assistant', content: content }, finish_reason: choice.finish_reason || 'stop' }], usage: data.usage || { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 } }; } // ===== ROUTES ===== app.use(express.static(path.join(__dirname, 'public'))); app.get('/', (req, res) => { res.sendFile(path.join(__dirname, 'public', 'index.html')); }); // Health check (Không bị rate limit) app.get('/health', (req, res) => { res.json({ status: 'healthy', timestamp: new Date().toISOString(), uptime: process.uptime(), models: Object.keys(MODEL_MAPPING).length, api_key_configured: !!ZEN_API_KEY, memory_usage_mb: Math.round(process.memoryUsage().rss / 1024 / 1024), // Theo dõi RAM thực tế environment: process.env.NODE_ENV || 'production' }); }); app.get('/api', (req, res) => { res.json({ name: 'OpenCode Free Proxy (Render Optimized)', version: '2.0.0', status: 'running', rate_limit: '20 requests per minute per IP' }); }); // ===== API ENDPOINTS (CÓ RATE LIMIT) ===== app.get('/v1/models', apiLimiter, (req, res) => { res.json({ object: 'list', data: MODEL_LIST }); }); app.post('/v1/chat/completions', apiLimiter, async (req, res) => { if (!ZEN_API_KEY) { return res.status(500).json({ error: { message: 'ZEN_API_KEY is not configured.', type: 'server_error' } }); } const { model, messages, temperature, max_tokens, stream, reasoning_effort } = req.body; if (!messages || !Array.isArray(messages) || messages.length === 0) { return res.status(400).json({ error: { message: 'Messages are required', type: 'invalid_request_error' } }); } try { let zenModel = MODEL_MAPPING[model]; if (!zenModel) { if (model?.startsWith('opencode/')) { zenModel = MODEL_MAPPING[model.replace('opencode/', '')]; } zenModel = zenModel || 'mimo-v2.5-free'; } let requestMaxTokens = Math.min(max_tokens || 1024, 4096); if (zenModel === 'deepseek-v4-flash-free') { requestMaxTokens = Math.min(max_tokens || 2048, 8192); } const payload = { model: zenModel, messages, temperature: temperature ?? 0.7, max_tokens: requestMaxTokens, stream: stream || false }; if (reasoning_effort) payload.reasoning_effort = reasoning_effort; // Sử dụng apiInstance đã tối ưu Keep-Alive & Timeout const response = await apiInstance.post('/chat/completions', payload, { responseType: stream ? 'stream' : 'json' }); if (stream) { // Truyền thêm req để bắt sự kiện người dùng ngắt kết nối return handleStream(req, response, res, model); } return handleResponse(response.data, res, model); } catch (error) { console.error('API Error:', error.message); const status = error.response?.status || 500; const message = error.response?.data?.error?.message || error.message || 'Server error'; res.status(status).json({ error: { message: message, type: 'api_error' } }); } }); // ===== STREAMING HANDLER (TỐI ƯU CHỐNG RÒ RỈ RAM) ===== function handleStream(req, response, res, originalModel) { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Accel-Buffering', 'no'); let buffer = ''; // [QUAN TRỌNG] Nếu user tắt tab/trình duyệt giữa chừng -> Hủy ngay stream từ upstream // Giúp Node.js không bị ép tải tiếp data về RAM gây tràn bộ nhớ (OOM) req.on('close', () => { if (response.data && typeof response.data.destroy === 'function') { response.data.destroy(); } }); response.data.on('data', chunk => { buffer += chunk.toString(); const lines = buffer.split('\n'); buffer = lines.pop() || ''; for (const line of lines) { if (!line.startsWith('data: ')) continue; if (line.includes('[DONE]')) { res.write('data: [DONE]\n\n'); continue; } try { const data = JSON.parse(line.slice(6)); const delta = data.choices?.[0]?.delta; if (delta) { const extractedContent = extractContent(delta); if (extractedContent) { data.choices[0].delta.content = extractedContent; } } res.write(`data: ${JSON.stringify(data)}\n\n`); } catch { res.write(`${line}\n`); } } }); response.data.on('end', () => res.end()); response.data.on('error', (err) => { console.error('Stream error:', err.message); res.write(`data: ${JSON.stringify({ error: 'Stream interrupted from upstream' })}\n\n`); res.end(); }); } // ===== NON-STREAMING HANDLER ===== function handleResponse(data, res, originalModel) { const normalized = normalizeResponse(data, originalModel); res.json(normalized); } // 404 handler app.all('*', (req, res) => { res.status(404).json({ error: { message: `Endpoint ${req.method} ${req.path} not found`, type: 'invalid_request_error' } }); }); // ===== ERROR HANDLING ===== app.use((err, req, res, next) => { console.error('Server Error:', err.message); res.status(500).json({ error: { message: 'Internal server error', type: 'server_error' } }); }); // ===== KHỞI TẠO SERVER ===== app.listen(PORT, '0.0.0.0', () => { console.log(`🚀 Server running on port ${PORT}`); console.log(`🛡️ Trust Proxy: Enabled (Render Ready)`); console.log(`🗜️ Compression & Keep-Alive: Enabled`); console.log(`⏱️ Rate Limit: 20 requests/minute/IP`); }); module.exports = app;