| 1 | import express from 'express'; |
| 2 | import fetch from 'node-fetch'; |
| 3 | import mime from 'mime-types'; |
| 4 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 5 | import { OPENROUTER_HEADERS } from '../constants.js'; |
| 6 | |
| 7 | export const router = express.Router(); |
| 8 | const API_OPENROUTER = 'https://openrouter.ai/api/v1'; |
| 9 | |
| 10 | router.post('/models/providers', async (req, res) => { |
| 11 | try { |
| 12 | const { model } = req.body; |
| 13 | const response = await fetch(`${API_OPENROUTER}/models/${model}/endpoints`, { |
| 14 | method: 'GET', |
| 15 | headers: { |
| 16 | 'Accept': 'application/json', |
| 17 | }, |
| 18 | }); |
| 19 | |
| 20 | if (!response.ok) { |
| 21 | return res.json([]); |
| 22 | } |
| 23 | |
| 24 | /** @type {any} */ |
| 25 | const data = await response.json(); |
| 26 | const endpoints = data?.data?.endpoints || []; |
| 27 | const providerNames = endpoints.map(e => e.provider_name); |
| 28 | |
| 29 | return res.json(providerNames); |
| 30 | } catch (error) { |
| 31 | console.error(error); |
| 32 | return res.sendStatus(500); |
| 33 | } |
| 34 | }); |
| 35 | |
| 36 | /** |
| 37 | * Fetches and filters models from OpenRouter API based on modality criteria. |
| 38 | * @param {string} endpoint - The API endpoint to fetch from |
| 39 | * @param {string} inputModality - Required input modality |
| 40 | * @param {string} outputModality - Required output modality |
| 41 | * @param {((model: any) => any) | null} [mapFn=null] - Optional mapping function to transform the results |
| 42 | * @returns {Promise<any[]>} Filtered and/or mapped models |
| 43 | */ |
| 44 | async function fetchModelsByModality(endpoint, inputModality, outputModality, mapFn = null) { |
| 45 | const response = await fetch(`${API_OPENROUTER}${endpoint}?output_modalities=${encodeURIComponent(outputModality)}`, { |
| 46 | method: 'GET', |
| 47 | headers: { 'Accept': 'application/json' }, |
| 48 | }); |
| 49 | |
| 50 | if (!response.ok) { |
| 51 | console.warn('OpenRouter API request failed', response.statusText); |
| 52 | return []; |
| 53 | } |
| 54 | |
| 55 | /** @type {any} */ |
| 56 | const data = await response.json(); |
| 57 | |
| 58 | if (!Array.isArray(data?.data)) { |
| 59 | console.warn('OpenRouter API response was not an array'); |
| 60 | return []; |
| 61 | } |
| 62 | |
| 63 | const filtered = data.data |
| 64 | .filter(m => Array.isArray(m?.architecture?.input_modalities)) |
| 65 | .filter(m => m.architecture.input_modalities.includes(inputModality)) |
| 66 | .filter(m => Array.isArray(m?.architecture?.output_modalities)) |
| 67 | .filter(m => m.architecture.output_modalities.includes(outputModality)) |
| 68 | .sort((a, b) => a?.id && b?.id ? a.id.localeCompare(b.id) : 0); |
| 69 | |
| 70 | return typeof mapFn === 'function' ? filtered.map(mapFn) : filtered; |
| 71 | } |
| 72 | |
| 73 | router.post('/models/multimodal', async (_req, res) => { |
| 74 | try { |
| 75 | const models = await fetchModelsByModality('/models', 'image', 'text', m => m.id); |
| 76 | return res.json(models); |
| 77 | } catch (error) { |
| 78 | console.error(error); |
| 79 | return res.sendStatus(500); |
| 80 | } |
| 81 | }); |
| 82 | |
| 83 | router.post('/models/embedding', async (_req, res) => { |
| 84 | try { |
| 85 | const models = await fetchModelsByModality('/models', 'text', 'embeddings', m => ({ id: m.id, name: m.name })); |
| 86 | return res.json(models); |
| 87 | } catch (error) { |
| 88 | console.error(error); |
| 89 | return res.sendStatus(500); |
| 90 | } |
| 91 | }); |
| 92 | |
| 93 | router.post('/models/image', async (_req, res) => { |
| 94 | try { |
| 95 | const models = await fetchModelsByModality('/models', 'text', 'image', m => ({ value: m.id, text: m.name || m.id })); |
| 96 | return res.json(models); |
| 97 | } catch (error) { |
| 98 | console.error(error); |
| 99 | return res.sendStatus(500); |
| 100 | } |
| 101 | }); |
| 102 | |
| 103 | router.post('/credits', async (req, res) => { |
| 104 | try { |
| 105 | const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); |
| 106 | |
| 107 | if (!key) { |
| 108 | console.warn('OpenRouter API key not found'); |
| 109 | return res.sendStatus(400); |
| 110 | } |
| 111 | |
| 112 | const response = await fetch(`${API_OPENROUTER}/credits`, { |
| 113 | method: 'GET', |
| 114 | headers: { |
| 115 | 'Accept': 'application/json', |
| 116 | 'Authorization': `Bearer ${key}`, |
| 117 | }, |
| 118 | }); |
| 119 | |
| 120 | if (!response.ok) { |
| 121 | console.warn('OpenRouter credits request failed', response.statusText); |
| 122 | return res.sendStatus(500); |
| 123 | } |
| 124 | |
| 125 | /** @type {any} */ |
| 126 | const data = await response.json(); |
| 127 | const totalCredits = data.data?.total_credits ?? 0; |
| 128 | const totalUsage = data.data?.total_usage ?? 0; |
| 129 | const remaining = totalCredits - totalUsage; |
| 130 | |
| 131 | return res.json({ remaining, total_credits: totalCredits, total_usage: totalUsage }); |
| 132 | } catch (error) { |
| 133 | console.error(error); |
| 134 | return res.sendStatus(500); |
| 135 | } |
| 136 | }); |
| 137 | |
| 138 | router.post('/image/generate', async (req, res) => { |
| 139 | try { |
| 140 | const key = readSecret(req.user.directories, SECRET_KEYS.OPENROUTER); |
| 141 | |
| 142 | if (!key) { |
| 143 | console.warn('OpenRouter API key not found'); |
| 144 | return res.status(400).json({ error: 'OpenRouter API key not found' }); |
| 145 | } |
| 146 | |
| 147 | console.debug('OpenRouter image generation request', req.body); |
| 148 | |
| 149 | const { model, prompt } = req.body; |
| 150 | |
| 151 | if (!model || !prompt) { |
| 152 | return res.status(400).json({ error: 'Model and prompt are required' }); |
| 153 | } |
| 154 | |
| 155 | const response = await fetch(`${API_OPENROUTER}/chat/completions`, { |
| 156 | method: 'POST', |
| 157 | headers: { |
| 158 | ...OPENROUTER_HEADERS, |
| 159 | 'Content-Type': 'application/json', |
| 160 | 'Authorization': `Bearer ${key}`, |
| 161 | }, |
| 162 | body: JSON.stringify({ |
| 163 | model: model, |
| 164 | messages: [ |
| 165 | { |
| 166 | role: 'user', |
| 167 | content: prompt, |
| 168 | }, |
| 169 | ], |
| 170 | modalities: ['image'], |
| 171 | image_config: { |
| 172 | aspect_ratio: req.body.aspect_ratio || '1:1', |
| 173 | }, |
| 174 | }), |
| 175 | }); |
| 176 | |
| 177 | if (!response.ok) { |
| 178 | console.warn('OpenRouter image generation failed', await response.text()); |
| 179 | return res.sendStatus(500); |
| 180 | } |
| 181 | |
| 182 | /** @type {any} */ |
| 183 | const data = await response.json(); |
| 184 | |
| 185 | const imageUrl = data?.choices?.[0]?.message?.images?.[0]?.image_url?.url; |
| 186 | |
| 187 | if (!imageUrl) { |
| 188 | console.warn('No image URL found in OpenRouter response', data); |
| 189 | return res.sendStatus(500); |
| 190 | } |
| 191 | |
| 192 | const [mimeType, base64Data] = /^data:(.*);base64,(.*)$/.exec(imageUrl)?.slice(1) || []; |
| 193 | |
| 194 | if (!mimeType || !base64Data) { |
| 195 | console.warn('Invalid image data format', imageUrl); |
| 196 | return res.sendStatus(500); |
| 197 | } |
| 198 | |
| 199 | const result = { |
| 200 | format: mime.extension(mimeType) || 'png', |
| 201 | image: base64Data, |
| 202 | }; |
| 203 | |
| 204 | return res.json(result); |
| 205 | } catch (error) { |
| 206 | console.error(error); |
| 207 | return res.sendStatus(500); |
| 208 | } |
| 209 | }); |