| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const express = require('express'); |
| const fetch = require('node-fetch'); |
| const fs = require('fs'); |
| const path = require('path'); |
|
|
| const app = express(); |
| app.use(express.json()); |
|
|
| |
| const configPath = path.join(__dirname, '..', 'config', 'omniroute-masterfile.json'); |
| let masterfile = { providers: {}, model: {}, routing: {} }; |
|
|
| function loadConfig() { |
| if (fs.existsSync(configPath)) { |
| const raw = fs.readFileSync(configPath, 'utf-8'); |
| const resolved = raw.replace(/\$\{(\w+)\}/g, (_, key) => process.env[key] || ''); |
| masterfile = JSON.parse(resolved); |
| console.log(`π‘ OmniRoute: Tiered Masterfile loaded.`); |
| } |
| } |
| loadConfig(); |
|
|
| |
| const PORT = process.env.GRAPH_VIEWER_PORT ? 20128 : (process.env.PORT || 20128); |
| const keyRotation = {}; |
| const keyCooldowns = {}; |
|
|
| |
|
|
| |
| |
| |
| function toAnthropic(messages) { |
| const system = messages.find(m => m.role === 'system')?.content || ''; |
| const contents = messages.filter(m => m.role !== 'system').map(m => ({ |
| role: m.role === 'assistant' ? 'assistant' : 'user', |
| content: m.content |
| })); |
| return { system, messages: contents }; |
| } |
|
|
| |
| |
| |
| function toGemini(messages) { |
| return { |
| contents: messages.map(m => ({ |
| role: m.role === 'assistant' ? 'model' : 'user', |
| parts: [{ text: m.content }] |
| })) |
| }; |
| } |
|
|
| |
|
|
| function getAvailableKeys(providerName) { |
| const base = `${providerName.toUpperCase()}_API_KEY`; |
| const keys = []; |
| if (process.env[base] && process.env[base] !== 'REPLACE_ME') keys.push(process.env[base]); |
| for (let i = 2; i <= 10; i++) { |
| const k = process.env[`${base}_${i}`]; |
| if (k && k !== 'REPLACE_ME') keys.push(k); |
| } |
| return keys; |
| } |
|
|
| function getKeyCooldown(provider, key) { |
| const composite = `${provider}_${key.slice(-5)}`; |
| if (keyCooldowns[composite] && Date.now() < keyCooldowns[composite]) return true; |
| return false; |
| } |
|
|
| function setKeyCooldown(provider, key, seconds = 60) { |
| const composite = `${provider}_${key.slice(-5)}`; |
| keyCooldowns[composite] = Date.now() + (seconds * 1000); |
| console.log(`βΈοΈ Key ${composite} limited for ${seconds}s`); |
| } |
|
|
| |
| |
| |
| function findCandidates(requestedModel) { |
| const candidates = []; |
| for (const [pName, pConfig] of Object.entries(masterfile.providers)) { |
| const keys = getAvailableKeys(pName); |
| if (keys.length === 0) continue; |
|
|
| for (const model of pConfig.models) { |
| if (requestedModel && model.id !== requestedModel) continue; |
| |
| candidates.push({ |
| provider: pName, |
| model: model.id, |
| api: pConfig.api, |
| baseUrl: pConfig.baseUrl, |
| tier: pConfig.tier || 3, |
| priority: model.priority || 99, |
| keys |
| }); |
| } |
| } |
|
|
| |
| candidates.sort((a, b) => a.tier - b.tier || a.priority - b.priority); |
| return candidates; |
| } |
|
|
| |
|
|
| app.post('/v1/chat/completions', async (req, res) => { |
| const { model: reqModel, messages, ...options } = req.body; |
| const chain = reqModel ? [reqModel] : [masterfile.model.primary, masterfile.model.fallback, masterfile.model.review]; |
| |
| for (const targetModel of chain) { |
| const candidates = findCandidates(targetModel); |
|
|
| for (const c of candidates) { |
| |
| if (!keyRotation[c.provider]) keyRotation[c.provider] = 0; |
| |
| for (let i = 0; i < c.keys.length; i++) { |
| const keyIdx = (keyRotation[c.provider] + i) % c.keys.length; |
| const key = c.keys[keyIdx]; |
|
|
| if (getKeyCooldown(c.provider, key)) continue; |
|
|
| try { |
| console.log(`π‘ [Tier ${c.tier}] ${c.provider} -> ${targetModel}`); |
| const controller = new AbortController(); |
| const timeout = setTimeout(() => controller.abort(), 60000); |
|
|
| let body, url, headers = { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' }; |
|
|
| |
| if (c.api === 'anthropic-messages') { |
| const translated = toAnthropic(messages); |
| url = `${c.baseUrl}/messages`; |
| headers['x-api-key'] = key; |
| delete headers['Authorization']; |
| body = { model: c.model, ...translated, max_tokens: options.max_tokens || 4096 }; |
| } else if (c.api === 'google-gemini') { |
| const translated = toGemini(messages); |
| url = `${c.baseUrl}/${c.model}:generateContent?key=${key}`; |
| delete headers['Authorization']; |
| body = translated; |
| } else { |
| |
| url = `${c.baseUrl}/chat/completions`; |
| body = { model: c.model, messages, ...options }; |
| } |
|
|
| const response = await fetch(url, { |
| method: 'POST', |
| headers, |
| body: JSON.stringify(body), |
| signal: controller.signal |
| }); |
| clearTimeout(timeout); |
|
|
| if (response.status === 429) { |
| setKeyCooldown(c.provider, key, 120); |
| continue; |
| } |
|
|
| if (!response.ok) { |
| console.warn(`β οΈ ${c.provider} failed (${response.status})`); |
| continue; |
| } |
|
|
| const data = await response.json(); |
| |
| keyRotation[c.provider] = (keyIdx + 1) % c.keys.length; |
| |
| |
| return res.json(data); |
|
|
| } catch (e) { |
| console.error(`β ${c.provider} error: ${e.message}`); |
| continue; |
| } |
| } |
| } |
| } |
|
|
| res.status(503).json({ error: "All tiers exhausted. Check API keys or cooldowns." }); |
| }); |
|
|
| |
|
|
| app.get('/v1/models', (req, res) => { |
| const list = []; |
| for (const [pName, pConfig] of Object.entries(masterfile.providers)) { |
| const keys = getAvailableKeys(pName); |
| pConfig.models.forEach(m => list.push({ |
| id: m.id, |
| tier: pConfig.tier, |
| provider: pName, |
| active_keys: keys.length, |
| available: !keys.every(k => getKeyCooldown(pName, k)) |
| })); |
| } |
| res.json({ data: list.sort((a,b) => a.tier - b.tier) }); |
| }); |
|
|
| app.listen(PORT, '0.0.0.0', () => { |
| console.log(`π OmniRoute v2: Polyglot Intelligence Engine on port ${PORT}`); |
| }); |
|
|