Blame Raw
Cohee · 51ad27fb · · 209 lines (6.7 KB)
2 contributors
1import express from 'express';
2import fetch from 'node-fetch';
3import mime from 'mime-types';
4import { readSecret, SECRET_KEYS } from './secrets.js';
5import { OPENROUTER_HEADERS } from '../constants.js';
6
7export const router = express.Router();
8const API_OPENROUTER = 'https://openrouter.ai/api/v1';
9
10router.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 */
44async 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
73router.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
83router.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
93router.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
103router.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
138router.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});