| |
| 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(); |
|
|
| |
| |
| |
| app.set('trust proxy', 1); |
|
|
| |
| |
| app.use(compression()); |
|
|
| |
| |
| |
| 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'] |
| })); |
|
|
| |
| |
| app.use(express.json({ limit: '1mb' })); |
|
|
| |
| 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!'); |
| } |
|
|
| |
| |
| 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, |
| httpAgent, |
| httpsAgent, |
| headers: { |
| 'Authorization': `Bearer ${ZEN_API_KEY}`, |
| 'Content-Type': 'application/json' |
| }, |
| validateStatus: status => status < 500 |
| }); |
|
|
| |
| const apiLimiter = rateLimit({ |
| windowMs: 60 * 1000, |
| max: 20, |
| 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) => { |
| |
| 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 |
| } |
| }); |
| } |
| }); |
|
|
| |
| 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: [] |
| })); |
|
|
| |
| 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; |
| } |
|
|
| |
| 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 } |
| }; |
| } |
|
|
| |
| app.use(express.static(path.join(__dirname, 'public'))); |
|
|
| app.get('/', (req, res) => { |
| res.sendFile(path.join(__dirname, 'public', 'index.html')); |
| }); |
|
|
| |
| 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), |
| 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' |
| }); |
| }); |
|
|
| |
| 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; |
|
|
| |
| const response = await apiInstance.post('/chat/completions', payload, { |
| responseType: stream ? 'stream' : 'json' |
| }); |
|
|
| if (stream) { |
| |
| 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' } |
| }); |
| } |
| }); |
|
|
| |
| 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 = ''; |
|
|
| |
| |
| 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(); |
| }); |
| } |
|
|
| |
| function handleResponse(data, res, originalModel) { |
| const normalized = normalizeResponse(data, originalModel); |
| res.json(normalized); |
| } |
|
|
| |
| app.all('*', (req, res) => { |
| res.status(404).json({ |
| error: { message: `Endpoint ${req.method} ${req.path} not found`, type: 'invalid_request_error' } |
| }); |
| }); |
|
|
| |
| app.use((err, req, res, next) => { |
| console.error('Server Error:', err.message); |
| res.status(500).json({ |
| error: { message: 'Internal server error', type: 'server_error' } |
| }); |
| }); |
|
|
| |
| 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; |