Blame Raw
permissionBRICK · 513b8d68 · · 2208 lines (72.1 KB)
4 contributors
1import fs from 'node:fs';
2import path from 'node:path';
3
4import express from 'express';
5import fetch from 'node-fetch';
6import sanitize from 'sanitize-filename';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import urlJoin from 'url-join';
9import _ from 'lodash';
10import mime from 'mime-types';
11
12import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';
13import { readSecret, SECRET_KEYS } from './secrets.js';
14import { getFileNameValidationFunction } from '../middleware/validateFileName.js';
15import { AIMLAPI_HEADERS } from '../constants.js';
16
17/**
18 * Gets the comfy workflows.
19 * @param {import('../users.js').UserDirectoryList} directories
20 * @returns {string[]} List of comfy workflows
21 */
22function getComfyWorkflows(directories) {
23 return fs
24 .readdirSync(directories.comfyWorkflows)
25 .filter(file => file[0] !== '.' && file.toLowerCase().endsWith('.json'))
26 .sort(Intl.Collator().compare);
27}
28
29export const router = express.Router();
30
31router.post('/ping', async (request, response) => {
32 try {
33 const url = new URL(request.body.url);
34 url.pathname = '/sdapi/v1/options';
35
36 const result = await fetch(url, {
37 method: 'GET',
38 headers: {
39 'Authorization': getBasicAuthHeader(request.body.auth),
40 },
41 });
42
43 if (!result.ok) {
44 throw new Error('SD WebUI returned an error.');
45 }
46
47 return response.sendStatus(200);
48 } catch (error) {
49 console.error(error);
50 return response.sendStatus(500);
51 }
52});
53
54router.post('/upscalers', async (request, response) => {
55 try {
56 async function getUpscalerModels() {
57 const url = new URL(request.body.url);
58 url.pathname = '/sdapi/v1/upscalers';
59
60 const result = await fetch(url, {
61 method: 'GET',
62 headers: {
63 'Authorization': getBasicAuthHeader(request.body.auth),
64 },
65 });
66
67 if (!result.ok) {
68 throw new Error('SD WebUI returned an error.');
69 }
70
71 /** @type {any} */
72 const data = await result.json();
73 return data.map(x => x.name);
74 }
75
76 async function getLatentUpscalers() {
77 const url = new URL(request.body.url);
78 url.pathname = '/sdapi/v1/latent-upscale-modes';
79
80 const result = await fetch(url, {
81 method: 'GET',
82 headers: {
83 'Authorization': getBasicAuthHeader(request.body.auth),
84 },
85 });
86
87 if (!result.ok) {
88 throw new Error('SD WebUI returned an error.');
89 }
90
91 /** @type {any} */
92 const data = await result.json();
93 return data.map(x => x.name);
94 }
95
96 const [upscalers, latentUpscalers] = await Promise.all([getUpscalerModels(), getLatentUpscalers()]);
97
98 // 0 = None, then Latent Upscalers, then Upscalers
99 upscalers.splice(1, 0, ...latentUpscalers);
100
101 return response.send(upscalers);
102 } catch (error) {
103 console.error(error);
104 return response.sendStatus(500);
105 }
106});
107
108router.post('/vaes', async (request, response) => {
109 try {
110 const autoUrl = new URL(request.body.url);
111 autoUrl.pathname = '/sdapi/v1/sd-vae';
112 const forgeUrl = new URL(request.body.url);
113 forgeUrl.pathname = '/sdapi/v1/sd-modules';
114
115 const requestInit = {
116 method: 'GET',
117 headers: {
118 'Authorization': getBasicAuthHeader(request.body.auth),
119 },
120 };
121 const results = await Promise.allSettled([
122 fetch(autoUrl, requestInit).then(r => r.ok ? r.json() : Promise.reject(r.statusText)),
123 fetch(forgeUrl, requestInit).then(r => r.ok ? r.json() : Promise.reject(r.statusText)),
124 ]);
125
126 const data = results.find(r => r.status === 'fulfilled')?.value;
127
128 if (!Array.isArray(data)) {
129 throw new Error('SD WebUI returned an error.');
130 }
131
132 const names = data.map(x => x.model_name);
133 return response.send(names);
134 } catch (error) {
135 console.error(error);
136 return response.sendStatus(500);
137 }
138});
139
140router.post('/samplers', async (request, response) => {
141 try {
142 const url = new URL(request.body.url);
143 url.pathname = '/sdapi/v1/samplers';
144
145 const result = await fetch(url, {
146 method: 'GET',
147 headers: {
148 'Authorization': getBasicAuthHeader(request.body.auth),
149 },
150 });
151
152 if (!result.ok) {
153 throw new Error('SD WebUI returned an error.');
154 }
155
156 /** @type {any} */
157 const data = await result.json();
158 const names = data.map(x => x.name);
159 return response.send(names);
160 } catch (error) {
161 console.error(error);
162 return response.sendStatus(500);
163 }
164});
165
166router.post('/schedulers', async (request, response) => {
167 try {
168 const url = new URL(request.body.url);
169 url.pathname = '/sdapi/v1/schedulers';
170
171 const result = await fetch(url, {
172 method: 'GET',
173 headers: {
174 'Authorization': getBasicAuthHeader(request.body.auth),
175 },
176 });
177
178 if (!result.ok) {
179 throw new Error('SD WebUI returned an error.');
180 }
181
182 /** @type {any} */
183 const data = await result.json();
184 const names = data.map(x => x.name);
185 return response.send(names);
186 } catch (error) {
187 console.error(error);
188 return response.sendStatus(500);
189 }
190});
191
192router.post('/models', async (request, response) => {
193 try {
194 const url = new URL(request.body.url);
195 url.pathname = '/sdapi/v1/sd-models';
196
197 const result = await fetch(url, {
198 method: 'GET',
199 headers: {
200 'Authorization': getBasicAuthHeader(request.body.auth),
201 },
202 });
203
204 if (!result.ok) {
205 throw new Error('SD WebUI returned an error.');
206 }
207
208 /** @type {any} */
209 const data = await result.json();
210 const models = data.map(x => ({ value: x.title, text: x.title }));
211 return response.send(models);
212 } catch (error) {
213 console.error(error);
214 return response.sendStatus(500);
215 }
216});
217
218router.post('/get-model', async (request, response) => {
219 try {
220 const url = new URL(request.body.url);
221 url.pathname = '/sdapi/v1/options';
222
223 const result = await fetch(url, {
224 method: 'GET',
225 headers: {
226 'Authorization': getBasicAuthHeader(request.body.auth),
227 },
228 });
229 /** @type {any} */
230 const data = await result.json();
231 return response.send(data.sd_model_checkpoint);
232 } catch (error) {
233 console.error(error);
234 return response.sendStatus(500);
235 }
236});
237
238router.post('/set-model', async (request, response) => {
239 try {
240 async function getProgress() {
241 const url = new URL(request.body.url);
242 url.pathname = '/sdapi/v1/progress';
243
244 const result = await fetch(url, {
245 method: 'GET',
246 headers: {
247 'Authorization': getBasicAuthHeader(request.body.auth),
248 },
249 });
250 return await result.json();
251 }
252
253 const url = new URL(request.body.url);
254 url.pathname = '/sdapi/v1/options';
255
256 const options = {
257 sd_model_checkpoint: request.body.model,
258 };
259
260 const result = await fetch(url, {
261 method: 'POST',
262 body: JSON.stringify(options),
263 headers: {
264 'Content-Type': 'application/json',
265 'Authorization': getBasicAuthHeader(request.body.auth),
266 },
267 });
268
269 if (!result.ok) {
270 throw new Error('SD WebUI returned an error.');
271 }
272
273 const MAX_ATTEMPTS = 10;
274 const CHECK_INTERVAL = 2000;
275
276 for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
277 /** @type {any} */
278 const progressState = await getProgress();
279
280 const progress = progressState.progress;
281 const jobCount = progressState.state.job_count;
282 if (progress === 0.0 && jobCount === 0) {
283 break;
284 }
285
286 console.info(`Waiting for SD WebUI to finish model loading... Progress: ${progress}; Job count: ${jobCount}`);
287 await delay(CHECK_INTERVAL);
288 }
289
290 return response.sendStatus(200);
291 } catch (error) {
292 console.error(error);
293 return response.sendStatus(500);
294 }
295});
296
297router.post('/generate', async (request, response) => {
298 try {
299 try {
300 const optionsUrl = new URL(request.body.url);
301 optionsUrl.pathname = '/sdapi/v1/options';
302 const optionsResult = await fetch(optionsUrl, { headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } });
303 if (optionsResult.ok) {
304 const optionsData = /** @type {any} */ (await optionsResult.json());
305 const isForge = 'forge_preset' in optionsData;
306
307 if (!isForge) {
308 _.unset(request.body, 'override_settings.forge_additional_modules');
309 }
310 }
311 } catch (error) {
312 console.error('SD WebUI failed to get options:', error);
313 }
314
315 const controller = new AbortController();
316 request.socket.removeAllListeners('close');
317 request.socket.on('close', function () {
318 if (!response.writableEnded) {
319 const interruptUrl = new URL(request.body.url);
320 interruptUrl.pathname = '/sdapi/v1/interrupt';
321 fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } });
322 }
323 controller.abort();
324 });
325
326 console.debug('SD WebUI request:', request.body);
327 const txt2imgUrl = new URL(request.body.url);
328 txt2imgUrl.pathname = '/sdapi/v1/txt2img';
329 const result = await fetch(txt2imgUrl, {
330 method: 'POST',
331 body: JSON.stringify(request.body),
332 headers: {
333 'Content-Type': 'application/json',
334 'Authorization': getBasicAuthHeader(request.body.auth),
335 },
336 signal: controller.signal,
337 });
338
339 if (!result.ok) {
340 const text = await result.text();
341 throw new Error('SD WebUI returned an error.', { cause: text });
342 }
343
344 const data = await result.json();
345 return response.send(data);
346 } catch (error) {
347 console.error(error);
348 return response.sendStatus(500);
349 }
350});
351
352router.post('/sd-next/upscalers', async (request, response) => {
353 try {
354 const url = new URL(request.body.url);
355 url.pathname = '/sdapi/v1/upscalers';
356
357 const result = await fetch(url, {
358 method: 'GET',
359 headers: {
360 'Authorization': getBasicAuthHeader(request.body.auth),
361 },
362 });
363
364 if (!result.ok) {
365 throw new Error('SD WebUI returned an error.');
366 }
367
368 // Vlad doesn't provide Latent Upscalers in the API, so we have to hardcode them here
369 const latentUpscalers = ['Latent', 'Latent (antialiased)', 'Latent (bicubic)', 'Latent (bicubic antialiased)', 'Latent (nearest)', 'Latent (nearest-exact)'];
370
371 /** @type {any} */
372 const data = await result.json();
373 const names = data.map(x => x.name);
374
375 // 0 = None, then Latent Upscalers, then Upscalers
376 names.splice(1, 0, ...latentUpscalers);
377
378 return response.send(names);
379 } catch (error) {
380 console.error(error);
381 return response.sendStatus(500);
382 }
383});
384
385const comfy = express.Router();
386
387comfy.post('/ping', async (request, response) => {
388 try {
389 const url = new URL(urlJoin(request.body.url, '/system_stats'));
390
391 const result = await fetch(url);
392 if (!result.ok) {
393 throw new Error('ComfyUI returned an error.');
394 }
395
396 return response.sendStatus(200);
397 } catch (error) {
398 console.error(error);
399 return response.sendStatus(500);
400 }
401});
402
403comfy.post('/samplers', async (request, response) => {
404 try {
405 const url = new URL(urlJoin(request.body.url, '/object_info'));
406
407 const result = await fetch(url);
408 if (!result.ok) {
409 throw new Error('ComfyUI returned an error.');
410 }
411
412 /** @type {any} */
413 const data = await result.json();
414 return response.send(data.KSampler.input.required.sampler_name[0]);
415 } catch (error) {
416 console.error(error);
417 return response.sendStatus(500);
418 }
419});
420
421comfy.post('/models', async (request, response) => {
422 try {
423 const url = new URL(urlJoin(request.body.url, '/object_info'));
424
425 const result = await fetch(url);
426 if (!result.ok) {
427 throw new Error('ComfyUI returned an error.');
428 }
429 /** @type {any} */
430 const data = await result.json();
431
432 const ckpts = data.CheckpointLoaderSimple.input.required.ckpt_name[0].map(it => ({ value: it, text: it })) || [];
433 const unets = data.UNETLoader.input.required.unet_name[0].map(it => ({ value: it, text: `UNet: ${it}` })) || [];
434
435 // load list of GGUF unets from diffusion_models if the loader node is available
436 const ggufs = data.UnetLoaderGGUF?.input.required.unet_name[0].map(it => ({ value: it, text: `GGUF: ${it}` })) || [];
437 const models = [...ckpts, ...unets, ...ggufs];
438
439 // make the display names of the models somewhat presentable
440 models.forEach(it => it.text = it.text.replace(/\.[^.]*$/, '').replace(/_/g, ' '));
441
442 return response.send(models);
443 } catch (error) {
444 console.error(error);
445 return response.sendStatus(500);
446 }
447});
448
449comfy.post('/schedulers', async (request, response) => {
450 try {
451 const url = new URL(urlJoin(request.body.url, '/object_info'));
452
453 const result = await fetch(url);
454 if (!result.ok) {
455 throw new Error('ComfyUI returned an error.');
456 }
457
458 /** @type {any} */
459 const data = await result.json();
460 return response.send(data.KSampler.input.required.scheduler[0]);
461 } catch (error) {
462 console.error(error);
463 return response.sendStatus(500);
464 }
465});
466
467comfy.post('/vaes', async (request, response) => {
468 try {
469 const url = new URL(urlJoin(request.body.url, '/object_info'));
470
471 const result = await fetch(url);
472 if (!result.ok) {
473 throw new Error('ComfyUI returned an error.');
474 }
475
476 /** @type {any} */
477 const data = await result.json();
478 return response.send(data.VAELoader.input.required.vae_name[0]);
479 } catch (error) {
480 console.error(error);
481 return response.sendStatus(500);
482 }
483});
484
485comfy.post('/workflows', async (request, response) => {
486 try {
487 const data = getComfyWorkflows(request.user.directories);
488 return response.send(data);
489 } catch (error) {
490 console.error(error);
491 return response.sendStatus(500);
492 }
493});
494
495comfy.post('/workflow', async (request, response) => {
496 try {
497 let filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name)));
498 if (!fs.existsSync(filePath)) {
499 filePath = path.join(request.user.directories.comfyWorkflows, 'Default_Comfy_Workflow.json');
500 }
501 const data = fs.readFileSync(filePath, { encoding: 'utf-8' });
502 return response.send(JSON.stringify(data));
503 } catch (error) {
504 console.error(error);
505 return response.sendStatus(500);
506 }
507});
508
509comfy.post('/save-workflow', async (request, response) => {
510 try {
511 const filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name)));
512 writeFileAtomicSync(filePath, request.body.workflow, 'utf8');
513 const data = getComfyWorkflows(request.user.directories);
514 return response.send(data);
515 } catch (error) {
516 console.error(error);
517 return response.sendStatus(500);
518 }
519});
520
521comfy.post('/delete-workflow', async (request, response) => {
522 try {
523 const filePath = path.join(request.user.directories.comfyWorkflows, sanitize(String(request.body.file_name)));
524 if (fs.existsSync(filePath)) {
525 fs.unlinkSync(filePath);
526 }
527 return response.sendStatus(200);
528 } catch (error) {
529 console.error(error);
530 return response.sendStatus(500);
531 }
532});
533
534comfy.post('/rename-workflow', getFileNameValidationFunction('old_name'), getFileNameValidationFunction('new_name'), async (request, response) => {
535 try {
536 const oldName = sanitize(String(request.body.old_name));
537 const newName = sanitize(String(request.body.new_name));
538
539 if (path.extname(oldName).toLowerCase() !== '.json' || path.extname(newName).toLowerCase() !== '.json') {
540 return response.status(400).send('Only JSON workflow files are allowed');
541 }
542
543 const oldPath = path.join(request.user.directories.comfyWorkflows, oldName);
544 const newPath = path.join(request.user.directories.comfyWorkflows, newName);
545
546 if (!fs.existsSync(oldPath)) {
547 return response.status(404).send('Workflow not found');
548 }
549
550 if (fs.existsSync(newPath)) {
551 return response.status(409).send('A workflow with that name already exists');
552 }
553
554 fs.renameSync(oldPath, newPath);
555 return response.sendStatus(204);
556 } catch (error) {
557 console.error('ComfyUI workflow rename failed', error);
558 return response.sendStatus(500);
559 }
560});
561
562comfy.post('/generate', async (request, response) => {
563 try {
564 let item;
565 const url = new URL(urlJoin(request.body.url, '/prompt'));
566
567 const controller = new AbortController();
568 request.socket.removeAllListeners('close');
569 request.socket.on('close', function () {
570 if (!response.writableEnded && !item) {
571 const interruptUrl = new URL(urlJoin(request.body.url, '/interrupt'));
572 fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': getBasicAuthHeader(request.body.auth) } });
573 }
574 controller.abort();
575 });
576
577 const promptResult = await fetch(url, {
578 method: 'POST',
579 body: request.body.prompt,
580 });
581 if (!promptResult.ok) {
582 const text = await promptResult.text();
583 throw new Error('ComfyUI returned an error.', { cause: tryParse(text) });
584 }
585
586 /** @type {any} */
587 const data = await promptResult.json();
588 const id = data.prompt_id;
589 const historyUrl = new URL(urlJoin(request.body.url, '/history'));
590 while (true) {
591 const result = await fetch(historyUrl);
592 if (!result.ok) {
593 throw new Error('ComfyUI returned an error.');
594 }
595 /** @type {any} */
596 const history = await result.json();
597 item = history[id];
598 if (item) {
599 break;
600 }
601 await delay(100);
602 }
603 if (item.status.status_str === 'error') {
604 // Report node tracebacks if available
605 const errorMessages = item.status?.messages
606 ?.filter(it => it[0] === 'execution_error')
607 .map(it => it[1])
608 .map(it => `${it.node_type} [${it.node_id}] ${it.exception_type}: ${it.exception_message}`)
609 .join('\n') || '';
610 throw new Error(`ComfyUI generation did not succeed.\n\n${errorMessages}`.trim());
611 }
612 const outputs = Object.keys(item.outputs).map(it => item.outputs[it]);
613 console.debug('ComfyUI outputs:', outputs);
614 const imgInfo = outputs.map(it => it.images).flat()[0] ?? outputs.map(it => it.gifs).flat()[0];
615 if (!imgInfo) {
616 throw new Error('ComfyUI did not return any recognizable outputs.');
617 }
618 const imgUrl = new URL(urlJoin(request.body.url, '/view'));
619 imgUrl.search = `?filename=${imgInfo.filename}&subfolder=${imgInfo.subfolder}&type=${imgInfo.type}`;
620 const imgResponse = await fetch(imgUrl);
621 if (!imgResponse.ok) {
622 throw new Error('ComfyUI returned an error.');
623 }
624 const format = path.extname(imgInfo.filename).slice(1).toLowerCase() || 'png';
625 const imgBuffer = await imgResponse.arrayBuffer();
626 return response.send({ format: format, data: Buffer.from(imgBuffer).toString('base64') });
627 } catch (error) {
628 console.error('ComfyUI error:', error);
629 response.status(500).send(error.message);
630 return response;
631 }
632});
633
634const comfyRunPod = express.Router();
635
636comfyRunPod.post('/ping', async (request, response) => {
637 try {
638 const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD);
639
640 if (!key) {
641 console.warn('RunPod key not found.');
642 return response.sendStatus(400);
643 }
644
645 const url = new URL(urlJoin(request.body.url, '/health'));
646
647 const result = await fetch(url, {
648 method: 'GET',
649 headers: { 'Authorization': `Bearer ${key}` },
650 });
651 if (!result.ok) {
652 throw new Error('ComfyUI returned an error.');
653 }
654 /** @type {any} */
655 const data = await result.json();
656 if (data.workers.ready <= 0) {
657 console.warn(`No workers reported as ready. ${result}`);
658 }
659
660 return response.sendStatus(200);
661 } catch (error) {
662 console.error(error);
663 return response.sendStatus(500);
664 }
665});
666
667comfyRunPod.post('/generate', async (request, response) => {
668 try {
669 const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD);
670
671 if (!key) {
672 console.warn('RunPod key not found.');
673 return response.sendStatus(400);
674 }
675
676 let jobId;
677 let item;
678 const url = new URL(urlJoin(request.body.url, '/run'));
679
680 const controller = new AbortController();
681 request.socket.removeAllListeners('close');
682 request.socket.on('close', function () {
683 if (!response.writableEnded && !item) {
684 const interruptUrl = new URL(urlJoin(request.body.url, `/cancel/${jobId}`));
685 fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${key}` } });
686 }
687 controller.abort();
688 });
689 const workflow = JSON.parse(request.body.prompt).prompt;
690 const wrappedWorkflow = workflow?.input?.workflow ? workflow : ({ input: { workflow: workflow } });
691 const runpodPrompt = JSON.stringify(wrappedWorkflow);
692
693 console.debug('ComfyUI RunPod request:', wrappedWorkflow);
694
695 const promptResult = await fetch(url, {
696 method: 'POST',
697 headers: { 'Authorization': `Bearer ${key}` },
698 body: runpodPrompt,
699 });
700 if (!promptResult.ok) {
701 const text = await promptResult.text();
702 throw new Error('ComfyUI returned an error.', { cause: tryParse(text) });
703 }
704
705 /** @type {any} */
706 const data = await promptResult.json();
707 jobId = data.id;
708 const statusUrl = new URL(urlJoin(request.body.url, `/status/${jobId}`));
709 while (true) {
710 const result = await fetch(statusUrl, {
711 method: 'GET',
712 headers: { 'Authorization': `Bearer ${key}` },
713 });
714 if (!result.ok) {
715 throw new Error('ComfyUI returned an error.');
716 }
717 /** @type {any} */
718 const status = await result.json();
719 if (status.output) {
720 item = status.output.images[0];
721 }
722 if (item) {
723 break;
724 }
725 await delay(500);
726 }
727 const format = path.extname(item.filename).slice(1).toLowerCase() || 'png';
728 return response.send({ format: format, data: item.data });
729 } catch (error) {
730 console.error('ComfyUI error:', error);
731 response.status(500).send(error.message);
732 return response;
733 }
734});
735
736const together = express.Router();
737
738together.post('/models', async (request, response) => {
739 try {
740 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
741
742 if (!key) {
743 console.warn('TogetherAI key not found.');
744 return response.sendStatus(400);
745 }
746
747 const modelsResponse = await fetch('https://api.together.xyz/api/models', {
748 method: 'GET',
749 headers: {
750 'Authorization': `Bearer ${key}`,
751 },
752 });
753
754 if (!modelsResponse.ok) {
755 console.warn('TogetherAI returned an error.');
756 return response.sendStatus(500);
757 }
758
759 const data = await modelsResponse.json();
760
761 if (!Array.isArray(data)) {
762 console.warn('TogetherAI returned invalid data.');
763 return response.sendStatus(500);
764 }
765
766 const models = data
767 .filter(x => x.type === 'image')
768 .map(x => ({ value: x.id, text: x.display_name }));
769
770 return response.send(models);
771 } catch (error) {
772 console.error(error);
773 return response.sendStatus(500);
774 }
775});
776
777together.post('/generate', async (request, response) => {
778 try {
779 const key = readSecret(request.user.directories, SECRET_KEYS.TOGETHERAI);
780
781 if (!key) {
782 console.warn('TogetherAI key not found.');
783 return response.sendStatus(400);
784 }
785
786 console.debug('TogetherAI request:', request.body);
787
788 const result = await fetch('https://api.together.xyz/v1/images/generations', {
789 method: 'POST',
790 body: JSON.stringify({
791 prompt: request.body.prompt,
792 negative_prompt: request.body.negative_prompt,
793 height: request.body.height,
794 width: request.body.width,
795 model: request.body.model,
796 steps: request.body.steps,
797 n: 1,
798 // Limited to 10000 on playground, works fine with more.
799 seed: request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000),
800 }),
801 headers: {
802 'Content-Type': 'application/json',
803 'Authorization': `Bearer ${key}`,
804 },
805 });
806
807 if (!result.ok) {
808 console.warn('TogetherAI returned an error.', { body: await result.text() });
809 return response.sendStatus(500);
810 }
811
812 /** @type {any} */
813 const data = await result.json();
814 console.debug('TogetherAI response:', data);
815
816 const choice = data?.data?.[0];
817 let b64_json = choice.b64_json;
818
819 if (!b64_json) {
820 const buffer = await (await fetch(choice.url)).arrayBuffer();
821 b64_json = Buffer.from(buffer).toString('base64');
822 }
823
824 return response.send({ format: 'jpg', data: b64_json });
825 } catch (error) {
826 console.error(error);
827 return response.sendStatus(500);
828 }
829});
830
831const sdcpp = express.Router();
832
833sdcpp.post('/ping', async (request, response) => {
834 try {
835 const url = new URL(urlJoin(request.body.url, '/v1/images/generations'));
836
837 const result = await fetch(url, { method: 'OPTIONS' });
838 if (!result.ok) {
839 throw new Error('stable-diffusion.cpp server returned an error.');
840 }
841
842 return response.sendStatus(200);
843 } catch (error) {
844 console.error(error);
845 return response.sendStatus(500);
846 }
847});
848
849sdcpp.post('/models', async (request, response) => {
850 try {
851 const url = new URL(urlJoin(request.body.url, '/v1/models'));
852
853 const result = await fetch(url);
854 if (!result.ok) {
855 throw new Error('stable-diffusion.cpp server returned an error.');
856 }
857
858 const data = await result.json();
859 return response.send(data);
860 } catch (error) {
861 console.error(error);
862 return response.sendStatus(500);
863 }
864});
865
866sdcpp.post('/generate', async (request, response) => {
867 try {
868 const url = new URL(urlJoin(request.body.url, '/sdapi/v1/txt2img'));
869
870 const payload = {
871 model: request.body.model,
872 prompt: request.body.prompt,
873 negative_prompt: request.body.negative_prompt,
874 width: request.body.width,
875 height: request.body.height,
876 steps: request.body.steps,
877 cfg_scale: request.body.cfg_scale,
878 seed: request.body.seed,
879 batch_size: request.body.batch_size,
880 sampler_name: request.body.sampler_name,
881 scheduler: request.body.scheduler,
882 // sd.cpp produces blank images when clip_skip is 1, which is the
883 // default (no skipping). Only send clip_skip when it's > 1.
884 clip_skip: request.body.clip_skip > 1 ? request.body.clip_skip : undefined,
885 };
886
887 for (const [key, value] of Object.entries(payload)) {
888 if (value === undefined || value === null || value === '') {
889 delete payload[key];
890 }
891 }
892
893 console.debug('stable-diffusion.cpp request:', payload);
894
895 const result = await fetch(url, {
896 method: 'POST',
897 body: JSON.stringify(payload),
898 headers: {
899 'Content-Type': 'application/json',
900 },
901 });
902
903 if (!result.ok) {
904 const text = await result.text();
905 throw new Error('stable-diffusion.cpp server returned an error.', { cause: text });
906 }
907
908 const data = await result.json();
909 return response.send(data);
910 } catch (error) {
911 console.error(error);
912 return response.sendStatus(500);
913 }
914});
915
916const drawthings = express.Router();
917
918drawthings.post('/ping', async (request, response) => {
919 try {
920 const url = new URL(request.body.url);
921 url.pathname = '/';
922
923 const result = await fetch(url, {
924 method: 'HEAD',
925 });
926
927 if (!result.ok) {
928 throw new Error('SD DrawThings API returned an error.');
929 }
930
931 return response.sendStatus(200);
932 } catch (error) {
933 console.error(error);
934 return response.sendStatus(500);
935 }
936});
937
938drawthings.post('/get-model', async (request, response) => {
939 try {
940 const url = new URL(request.body.url);
941 url.pathname = '/';
942
943 const result = await fetch(url, {
944 method: 'GET',
945 });
946
947 /** @type {any} */
948 const data = await result.json();
949
950 return response.send(data.model);
951 } catch (error) {
952 console.error(error);
953 return response.sendStatus(500);
954 }
955});
956
957drawthings.post('/get-upscaler', async (request, response) => {
958 try {
959 const url = new URL(request.body.url);
960 url.pathname = '/';
961
962 const result = await fetch(url, {
963 method: 'GET',
964 });
965
966 /** @type {any} */
967 const data = await result.json();
968
969 return response.send(data.upscaler);
970 } catch (error) {
971 console.error(error);
972 return response.sendStatus(500);
973 }
974});
975
976drawthings.post('/generate', async (request, response) => {
977 try {
978 console.debug('SD DrawThings API request:', request.body);
979
980 const url = new URL(request.body.url);
981 url.pathname = '/sdapi/v1/txt2img';
982
983 const body = { ...request.body };
984 const auth = getBasicAuthHeader(request.body.auth);
985 delete body.url;
986 delete body.auth;
987
988 const result = await fetch(url, {
989 method: 'POST',
990 body: JSON.stringify(body),
991 headers: {
992 'Content-Type': 'application/json',
993 'Authorization': auth,
994 },
995 });
996
997 if (!result.ok) {
998 const text = await result.text();
999 throw new Error('SD DrawThings API returned an error.', { cause: text });
1000 }
1001
1002 const data = await result.json();
1003 return response.send(data);
1004 } catch (error) {
1005 console.error(error);
1006 return response.sendStatus(500);
1007 }
1008});
1009
1010const pollinations = express.Router();
1011
1012pollinations.post('/models', async (_request, response) => {
1013 try {
1014 const modelsUrl = new URL('https://gen.pollinations.ai/image/models');
1015 const result = await fetch(modelsUrl);
1016
1017 if (!result.ok) {
1018 console.warn('Pollinations returned an error.', result.status, result.statusText);
1019 throw new Error('Pollinations request failed.');
1020 }
1021
1022 const data = await result.json();
1023
1024 if (!Array.isArray(data)) {
1025 console.warn('Pollinations returned invalid data.');
1026 throw new Error('Pollinations request failed.');
1027 }
1028
1029 const models = data.map(x => ({ value: x.name, text: x.name }));
1030 return response.send(models);
1031 } catch (error) {
1032 console.error(error);
1033 return response.sendStatus(500);
1034 }
1035});
1036
1037pollinations.post('/generate', async (request, response) => {
1038 try {
1039 const key = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
1040 if (!key) {
1041 console.warn('Pollinations API key not found.');
1042 return response.sendStatus(400);
1043 }
1044
1045 const promptUrl = new URL(`https://gen.pollinations.ai/image/${encodeURIComponent(request.body.prompt)}`);
1046 const params = new URLSearchParams({
1047 model: String(request.body.model),
1048 negative_prompt: String(request.body.negative_prompt),
1049 seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)),
1050 width: String(request.body.width ?? 1024),
1051 height: String(request.body.height ?? 1024),
1052 });
1053 if (request.body.enhance) {
1054 params.set('enhance', String(true));
1055 }
1056 promptUrl.search = params.toString();
1057
1058 console.info('Pollinations request URL:', promptUrl.toString());
1059
1060 const result = await fetch(promptUrl, {
1061 method: 'GET',
1062 headers: {
1063 'Authorization': `Bearer ${key}`,
1064 },
1065 });
1066
1067 if (!result.ok) {
1068 const text = await result.text();
1069 console.warn('Pollinations returned an error.', text);
1070 throw new Error('Pollinations request failed.');
1071 }
1072
1073 const format = result.headers.get('Content-Type')?.toString() || 'image/jpeg';
1074 const buffer = await result.arrayBuffer();
1075 return response.send({ image: Buffer.from(buffer).toString('base64'), format: mime.extension(format) || 'jpg' });
1076 } catch (error) {
1077 console.error(error);
1078 return response.sendStatus(500);
1079 }
1080});
1081
1082const stability = express.Router();
1083
1084stability.post('/generate', async (request, response) => {
1085 try {
1086 const key = readSecret(request.user.directories, SECRET_KEYS.STABILITY);
1087
1088 if (!key) {
1089 console.warn('Stability AI key not found.');
1090 return response.sendStatus(400);
1091 }
1092
1093 const { payload, model } = request.body;
1094
1095 console.debug('Stability AI request:', model, payload);
1096
1097 const formData = new FormData();
1098 for (const [key, value] of Object.entries(payload)) {
1099 if (value !== undefined) {
1100 formData.append(key, String(value));
1101 }
1102 }
1103
1104 let apiUrl;
1105 switch (model) {
1106 case 'stable-image-ultra':
1107 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/ultra';
1108 break;
1109 case 'stable-image-core':
1110 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/core';
1111 break;
1112 case 'stable-diffusion-3':
1113 apiUrl = 'https://api.stability.ai/v2beta/stable-image/generate/sd3';
1114 break;
1115 default:
1116 throw new Error('Invalid Stability AI model selected');
1117 }
1118
1119 const result = await fetch(apiUrl, {
1120 method: 'POST',
1121 headers: {
1122 'Authorization': `Bearer ${key}`,
1123 'Accept': 'image/*',
1124 },
1125 body: formData,
1126 });
1127
1128 if (!result.ok) {
1129 const text = await result.text();
1130 console.warn('Stability AI returned an error.', result.status, result.statusText, text);
1131 return response.sendStatus(500);
1132 }
1133
1134 const buffer = await result.arrayBuffer();
1135 return response.send(Buffer.from(buffer).toString('base64'));
1136 } catch (error) {
1137 console.error(error);
1138 return response.sendStatus(500);
1139 }
1140});
1141
1142const huggingface = express.Router();
1143
1144huggingface.post('/generate', async (request, response) => {
1145 try {
1146 const key = readSecret(request.user.directories, SECRET_KEYS.HUGGINGFACE);
1147
1148 if (!key) {
1149 console.warn('Hugging Face key not found.');
1150 return response.sendStatus(400);
1151 }
1152
1153 console.debug('Hugging Face request:', request.body);
1154
1155 const result = await fetch(`https://api-inference.huggingface.co/models/${request.body.model}`, {
1156 method: 'POST',
1157 body: JSON.stringify({
1158 inputs: request.body.prompt,
1159 }),
1160 headers: {
1161 'Content-Type': 'application/json',
1162 'Authorization': `Bearer ${key}`,
1163 },
1164 });
1165
1166 if (!result.ok) {
1167 console.warn('Hugging Face returned an error.');
1168 return response.sendStatus(500);
1169 }
1170
1171 const buffer = await result.arrayBuffer();
1172 return response.send({
1173 image: Buffer.from(buffer).toString('base64'),
1174 });
1175 } catch (error) {
1176 console.error(error);
1177 return response.sendStatus(500);
1178 }
1179});
1180
1181const electronhub = express.Router();
1182
1183electronhub.post('/models', async (request, response) => {
1184 try {
1185 const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
1186
1187 if (!key) {
1188 console.warn('Electron Hub key not found.');
1189 return response.sendStatus(400);
1190 }
1191
1192 const modelsResponse = await fetch('https://api.electronhub.ai/v1/models', {
1193 method: 'GET',
1194 headers: {
1195 'Authorization': `Bearer ${key}`,
1196 'Content-Type': 'application/json',
1197 },
1198 });
1199
1200 if (!modelsResponse.ok) {
1201 console.warn('Electron Hub returned an error.');
1202 return response.sendStatus(500);
1203 }
1204
1205 /** @type {any} */
1206 const data = await modelsResponse.json();
1207
1208 if (!Array.isArray(data?.data)) {
1209 console.warn('Electron Hub returned invalid data.');
1210 return response.sendStatus(500);
1211 }
1212
1213 const models = data.data
1214 .filter(x => x && Array.isArray(x.endpoints) && x.endpoints.includes('/v1/images/generations'))
1215 .map(x => ({ ...x, value: x.id, text: x.name }));
1216 return response.send(models);
1217 } catch (error) {
1218 console.error(error);
1219 return response.sendStatus(500);
1220 }
1221});
1222
1223electronhub.post('/generate', async (request, response) => {
1224 try {
1225 const key = readSecret(request.user.directories, SECRET_KEYS.ELECTRONHUB);
1226
1227 if (!key) {
1228 console.warn('Electron Hub key not found.');
1229 return response.sendStatus(400);
1230 }
1231
1232 let bodyParams = {
1233 model: request.body.model,
1234 prompt: request.body.prompt,
1235 response_format: 'b64_json',
1236 };
1237
1238 if (request.body.size) {
1239 bodyParams.size = request.body.size;
1240 }
1241
1242 if (request.body.quality) {
1243 bodyParams.quality = request.body.quality;
1244 }
1245
1246 console.debug('Electron Hub request:', bodyParams);
1247
1248 const result = await fetch('https://api.electronhub.ai/v1/images/generations', {
1249 method: 'POST',
1250 headers: {
1251 'Authorization': `Bearer ${key}`,
1252 'Content-Type': 'application/json',
1253 },
1254 body: JSON.stringify({
1255 ...bodyParams,
1256 }),
1257 });
1258
1259 if (!result.ok) {
1260 const errorText = await result.text();
1261 console.warn('Electron Hub returned an error.', result.status, result.statusText, errorText);
1262 return response.sendStatus(500);
1263 }
1264
1265 /** @type {any} */
1266 const data = await result.json();
1267 const image = data?.data?.[0]?.b64_json;
1268
1269 if (!image) {
1270 console.warn('Electron Hub returned invalid data.');
1271 return response.sendStatus(500);
1272 }
1273
1274 return response.send({ image });
1275 } catch (error) {
1276 console.error(error);
1277 return response.sendStatus(500);
1278 }
1279});
1280
1281electronhub.post('/sizes', async (request, response) => {
1282 const result = await fetch(`https://api.electronhub.ai/v1/models/${request.body.model}`, {
1283 method: 'GET',
1284 headers: {
1285 'Content-Type': 'application/json',
1286 },
1287 });
1288
1289 if (!result.ok) {
1290 console.warn('Electron Hub returned an error.');
1291 return response.sendStatus(500);
1292 }
1293
1294 /** @type {any} */
1295 const data = await result.json();
1296
1297 const sizes = data.sizes;
1298
1299 if (!sizes) {
1300 console.warn('Electron Hub returned invalid data.');
1301 return response.sendStatus(500);
1302 }
1303
1304 return response.send({ sizes });
1305});
1306
1307const chutes = express.Router();
1308
1309chutes.post('/models', async (request, response) => {
1310 try {
1311 const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1312
1313 if (!key) {
1314 console.warn('Chutes key not found.');
1315 return response.sendStatus(400);
1316 }
1317
1318 const modelsResponse = await fetch('https://api.chutes.ai/chutes/?template=diffusion&include_public=true&limit=999', {
1319 method: 'GET',
1320 headers: {
1321 'Authorization': `Bearer ${key}`,
1322 'Content-Type': 'application/json',
1323 },
1324 });
1325
1326 if (!modelsResponse.ok) {
1327 console.warn('Chutes returned an error.');
1328 return response.sendStatus(500);
1329 }
1330
1331 const data = await modelsResponse.json();
1332
1333 const chutesData = /** @type {{items: Array<{name: string}>}} */ (data);
1334 const models = chutesData.items.map(x => ({ value: x.name, text: x.name })).sort((a, b) => a?.text?.localeCompare(b?.text));
1335 return response.send(models);
1336 } catch (error) {
1337 console.error(error);
1338 return response.sendStatus(500);
1339 }
1340});
1341
1342chutes.post('/generate', async (request, response) => {
1343 try {
1344 const key = readSecret(request.user.directories, SECRET_KEYS.CHUTES);
1345
1346 if (!key) {
1347 console.warn('Chutes key not found.');
1348 return response.sendStatus(400);
1349 }
1350
1351 const bodyParams = {
1352 model: request.body.model,
1353 prompt: request.body.prompt,
1354 negative_prompt: request.body.negative_prompt,
1355 guidance_scale: request.body.guidance_scale || 7.0,
1356 width: request.body.width || 1024,
1357 height: request.body.height || 1024,
1358 num_inference_steps: request.body.steps || 10,
1359 };
1360
1361 console.debug('Chutes request:', bodyParams);
1362
1363 const result = await fetch('https://image.chutes.ai/generate', {
1364 method: 'POST',
1365 headers: {
1366 'Authorization': `Bearer ${key}`,
1367 'Content-Type': 'application/json',
1368 },
1369 body: JSON.stringify(bodyParams),
1370 });
1371
1372 if (!result.ok) {
1373 const text = await result.text();
1374 console.warn('Chutes returned an error:', text);
1375 return response.sendStatus(500);
1376 }
1377
1378 const buffer = await result.arrayBuffer();
1379 const base64 = Buffer.from(buffer).toString('base64');
1380
1381 return response.send({ image: base64 });
1382 } catch (error) {
1383 console.error(error);
1384 return response.sendStatus(500);
1385 }
1386});
1387
1388const nanogpt = express.Router();
1389
1390nanogpt.post('/models', async (request, response) => {
1391 try {
1392 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
1393
1394 if (!key) {
1395 console.warn('NanoGPT key not found.');
1396 return response.sendStatus(400);
1397 }
1398
1399 const modelsResponse = await fetch('https://nano-gpt.com/api/models', {
1400 method: 'GET',
1401 headers: {
1402 'x-api-key': key,
1403 'Content-Type': 'application/json',
1404 },
1405 });
1406
1407 if (!modelsResponse.ok) {
1408 console.warn('NanoGPT returned an error.');
1409 return response.sendStatus(500);
1410 }
1411
1412 /** @type {any} */
1413 const data = await modelsResponse.json();
1414 const imageModels = data?.models?.image;
1415
1416 if (!imageModels || typeof imageModels !== 'object') {
1417 console.warn('NanoGPT returned invalid data.');
1418 return response.sendStatus(500);
1419 }
1420
1421 const models = Object.values(imageModels).map(x => ({ value: x.model, text: x.name }));
1422 return response.send(models);
1423 } catch (error) {
1424 console.error(error);
1425 return response.sendStatus(500);
1426 }
1427});
1428
1429nanogpt.post('/generate', async (request, response) => {
1430 try {
1431 const key = readSecret(request.user.directories, SECRET_KEYS.NANOGPT);
1432
1433 if (!key) {
1434 console.warn('NanoGPT key not found.');
1435 return response.sendStatus(400);
1436 }
1437
1438 console.debug('NanoGPT request:', request.body);
1439
1440 const result = await fetch('https://nano-gpt.com/api/generate-image', {
1441 method: 'POST',
1442 body: JSON.stringify(request.body),
1443 headers: {
1444 'x-api-key': key,
1445 'Content-Type': 'application/json',
1446 },
1447 });
1448
1449 if (!result.ok) {
1450 console.warn('NanoGPT returned an error.');
1451 return response.sendStatus(500);
1452 }
1453
1454 /** @type {any} */
1455 const data = await result.json();
1456
1457 const image = data?.data?.[0]?.b64_json;
1458 if (!image) {
1459 console.warn('NanoGPT returned invalid data.');
1460 return response.sendStatus(500);
1461 }
1462
1463 return response.send({ image });
1464 } catch (error) {
1465 console.error(error);
1466 return response.sendStatus(500);
1467 }
1468});
1469
1470const bfl = express.Router();
1471
1472bfl.post('/generate', async (request, response) => {
1473 try {
1474 const key = readSecret(request.user.directories, SECRET_KEYS.BFL);
1475
1476 if (!key) {
1477 console.warn('BFL key not found.');
1478 return response.sendStatus(400);
1479 }
1480
1481 const requestBody = {
1482 prompt: request.body.prompt,
1483 steps: request.body.steps,
1484 guidance: request.body.guidance,
1485 width: request.body.width,
1486 height: request.body.height,
1487 prompt_upsampling: request.body.prompt_upsampling,
1488 seed: request.body.seed ?? null,
1489 safety_tolerance: 6, // being least strict
1490 output_format: 'jpeg',
1491 };
1492
1493 function getClosestAspectRatio(width, height) {
1494 const minAspect = 9 / 21;
1495 const maxAspect = 21 / 9;
1496 const currentAspect = width / height;
1497
1498 const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
1499 const simplifyRatio = (w, h) => {
1500 const divisor = gcd(w, h);
1501 return `${w / divisor}:${h / divisor}`;
1502 };
1503
1504 if (currentAspect < minAspect) {
1505 const adjustedHeight = Math.round(width / minAspect);
1506 return simplifyRatio(width, adjustedHeight);
1507 } else if (currentAspect > maxAspect) {
1508 const adjustedWidth = Math.round(height * maxAspect);
1509 return simplifyRatio(adjustedWidth, height);
1510 } else {
1511 return simplifyRatio(width, height);
1512 }
1513 }
1514
1515 if (String(request.body.model).endsWith('-ultra')) {
1516 requestBody.aspect_ratio = getClosestAspectRatio(request.body.width, request.body.height);
1517 delete requestBody.steps;
1518 delete requestBody.guidance;
1519 delete requestBody.width;
1520 delete requestBody.height;
1521 delete requestBody.prompt_upsampling;
1522 }
1523
1524 if (String(request.body.model).endsWith('-pro-1.1')) {
1525 delete requestBody.steps;
1526 delete requestBody.guidance;
1527 }
1528
1529 console.debug('BFL request:', requestBody);
1530
1531 const result = await fetch(`https://api.bfl.ml/v1/${request.body.model}`, {
1532 method: 'POST',
1533 body: JSON.stringify(requestBody),
1534 headers: {
1535 'Content-Type': 'application/json',
1536 'x-key': key,
1537 },
1538 });
1539
1540 if (!result.ok) {
1541 console.warn('BFL returned an error.');
1542 return response.sendStatus(500);
1543 }
1544
1545 /** @type {any} */
1546 const taskData = await result.json();
1547 const { id } = taskData;
1548
1549 const MAX_ATTEMPTS = 100;
1550 for (let i = 0; i < MAX_ATTEMPTS; i++) {
1551 await delay(2500);
1552
1553 const statusResult = await fetch(`https://api.bfl.ml/v1/get_result?id=${id}`);
1554
1555 if (!statusResult.ok) {
1556 const text = await statusResult.text();
1557 console.warn('BFL returned an error.', text);
1558 return response.sendStatus(500);
1559 }
1560
1561 /** @type {any} */
1562 const statusData = await statusResult.json();
1563
1564 if (statusData?.status === 'Pending') {
1565 continue;
1566 }
1567
1568 if (statusData?.status === 'Ready') {
1569 const { sample } = statusData.result;
1570 const fetchResult = await fetch(sample);
1571 const fetchData = await fetchResult.arrayBuffer();
1572 const image = Buffer.from(fetchData).toString('base64');
1573 return response.send({ image: image });
1574 }
1575
1576 throw new Error('BFL failed to generate image.', { cause: statusData });
1577 }
1578 } catch (error) {
1579 console.error(error);
1580 return response.sendStatus(500);
1581 }
1582});
1583
1584const falai = express.Router();
1585
1586falai.post('/models', async (_request, response) => {
1587 try {
1588 const modelsUrl = new URL('https://fal.ai/api/models?categories=text-to-image');
1589 let page = 1;
1590 /** @type {any} */
1591 let modelsResponse;
1592 let models = [];
1593
1594 do {
1595 modelsUrl.searchParams.set('page', page.toString());
1596 const result = await fetch(modelsUrl);
1597
1598 if (!result.ok) {
1599 console.warn('FAL.AI returned an error.', result.status, result.statusText);
1600 throw new Error('FAL.AI request failed.');
1601 }
1602
1603 modelsResponse = await result.json();
1604 if (!('items' in modelsResponse) || !Array.isArray(modelsResponse.items)) {
1605 console.warn('FAL.AI returned invalid data.');
1606 throw new Error('FAL.AI request failed.');
1607 }
1608
1609 models = models.concat(
1610 modelsResponse.items.filter(
1611 x => (
1612 !x.title.toLowerCase().includes('inpainting') &&
1613 !x.title.toLowerCase().includes('control') &&
1614 !x.title.toLowerCase().includes('upscale') &&
1615 !x.title.toLowerCase().includes('lora')
1616 ),
1617 ),
1618 );
1619
1620 page = modelsResponse.page + 1;
1621 } while (modelsResponse != null && page < modelsResponse.pages);
1622
1623 const modelOptions = models
1624 .sort((a, b) => a.title.localeCompare(b.title))
1625 .map(x => ({ value: x.modelUrl.split('fal-ai/')[1], text: x.title }))
1626 .map(x => ({ ...x, text: `${x.text} (${x.value})` }));
1627 return response.send(modelOptions);
1628 } catch (error) {
1629 console.error(error);
1630 return response.sendStatus(500);
1631 }
1632});
1633
1634falai.post('/generate', async (request, response) => {
1635 try {
1636 const key = readSecret(request.user.directories, SECRET_KEYS.FALAI);
1637
1638 if (!key) {
1639 console.warn('FAL.AI key not found.');
1640 return response.sendStatus(400);
1641 }
1642
1643 const requestBody = {
1644 prompt: request.body.prompt,
1645 image_size: { 'width': request.body.width, 'height': request.body.height },
1646 num_inference_steps: request.body.steps,
1647 seed: request.body.seed ?? null,
1648 guidance_scale: request.body.guidance,
1649 enable_safety_checker: false, // Disable general safety checks
1650 safety_tolerance: 6, // Make Flux the least strict
1651 };
1652
1653 console.debug('FAL.AI request:', requestBody);
1654
1655 const result = await fetch(`https://queue.fal.run/fal-ai/${request.body.model}`, {
1656 method: 'POST',
1657 body: JSON.stringify(requestBody),
1658 headers: {
1659 'Content-Type': 'application/json',
1660 'Authorization': `Key ${key}`,
1661 },
1662 });
1663
1664 if (!result.ok) {
1665 console.warn('FAL.AI returned an error.');
1666 return response.sendStatus(500);
1667 }
1668
1669 /** @type {any} */
1670 const taskData = await result.json();
1671 const { status_url } = taskData;
1672
1673 const MAX_ATTEMPTS = 100;
1674 for (let i = 0; i < MAX_ATTEMPTS; i++) {
1675 await delay(2500);
1676
1677 const statusResult = await fetch(status_url, {
1678 headers: {
1679 'Authorization': `Key ${key}`,
1680 },
1681 });
1682
1683 if (!statusResult.ok) {
1684 const text = await statusResult.text();
1685 console.warn('FAL.AI returned an error.', text);
1686 return response.sendStatus(500);
1687 }
1688
1689 /** @type {any} */
1690 const statusData = await statusResult.json();
1691
1692 if (statusData?.status === 'IN_QUEUE' || statusData?.status === 'IN_PROGRESS') {
1693 continue;
1694 }
1695
1696 if (statusData?.status === 'COMPLETED') {
1697 const resultFetch = await fetch(statusData?.response_url, {
1698 method: 'GET',
1699 headers: {
1700 'Authorization': `Key ${key}`,
1701 },
1702 });
1703 /** @type {any} */
1704 const resultData = await resultFetch.json();
1705
1706 if (resultData.detail !== null && resultData.detail !== undefined) {
1707 throw new Error('FAL.AI failed to generate image.', { cause: `${resultData.detail[0].loc[1]}: ${resultData.detail[0].msg}` });
1708 }
1709
1710 const imageFetch = await fetch(resultData?.images[0].url, {
1711 headers: {
1712 'Authorization': `Key ${key}`,
1713 },
1714 });
1715
1716 const fetchData = await imageFetch.arrayBuffer();
1717 const image = Buffer.from(fetchData).toString('base64');
1718 return response.send({ image: image });
1719 }
1720
1721 throw new Error('FAL.AI failed to generate image.', { cause: statusData });
1722 }
1723 } catch (error) {
1724 console.error(error);
1725 return response.status(500).send(error.cause || error.message);
1726 }
1727});
1728
1729const xai = express.Router();
1730
1731xai.post('/generate', async (request, response) => {
1732 try {
1733 const key = readSecret(request.user.directories, SECRET_KEYS.XAI);
1734
1735 if (!key) {
1736 console.warn('xAI key not found.');
1737 return response.sendStatus(400);
1738 }
1739
1740 const requestBody = {
1741 prompt: request.body.prompt,
1742 model: request.body.model,
1743 aspect_ratio: request.body.aspect_ratio,
1744 resolution: request.body.resolution,
1745 response_format: 'b64_json',
1746 };
1747
1748 console.debug('xAI request:', requestBody);
1749
1750 const result = await fetch('https://api.x.ai/v1/images/generations', {
1751 method: 'POST',
1752 body: JSON.stringify(requestBody),
1753 headers: {
1754 'Content-Type': 'application/json',
1755 'Authorization': `Bearer ${key}`,
1756 },
1757 });
1758
1759 if (!result.ok) {
1760 const text = await result.text();
1761 console.warn('xAI returned an error.', text);
1762 return response.sendStatus(500);
1763 }
1764
1765 /** @type {any} */
1766 const data = await result.json();
1767
1768 // Can either be a base64 buffer (always JPEG) or a data URL (with MIME type)
1769 const encodedImage = String(data?.data?.[0]?.b64_json || '');
1770 if (!encodedImage) {
1771 console.warn('xAI returned invalid data.');
1772 return response.sendStatus(500);
1773 }
1774
1775 const dataUrlMatch = encodedImage.match(/^data:(.+);base64,(.+)$/);
1776 const mimeType = dataUrlMatch?.[1] || 'image/jpeg';
1777 const format = mime.extension(mimeType) || 'jpg';
1778 const image = dataUrlMatch?.[2] || encodedImage;
1779
1780 return response.send({ image, format });
1781 } catch (error) {
1782 console.error('Error communicating with xAI', error);
1783 return response.sendStatus(500);
1784 }
1785});
1786
1787const aimlapi = express.Router();
1788
1789aimlapi.post('/models', async (request, response) => {
1790 try {
1791 const key = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
1792
1793 if (!key) {
1794 console.warn('AI/ML API key not found.');
1795 return response.sendStatus(400);
1796 }
1797
1798 const modelsResponse = await fetch('https://api.aimlapi.com/v1/models', {
1799 method: 'GET',
1800 headers: {
1801 Authorization: `Bearer ${key}`,
1802 },
1803 });
1804
1805 if (!modelsResponse.ok) {
1806 console.warn('AI/ML API returned an error.');
1807 return response.sendStatus(500);
1808 }
1809
1810 /** @type {any} */
1811 const data = await modelsResponse.json();
1812 const models = (data.data || [])
1813 .filter(model =>
1814 model.type === 'image' &&
1815 model.id !== 'triposr' &&
1816 model.id !== 'flux/dev/image-to-image',
1817 )
1818 .map(model => ({
1819 value: model.id,
1820 text: model.info?.name || model.id,
1821 }));
1822
1823 return response.send({ data: models });
1824 } catch (error) {
1825 console.error(error);
1826 return response.sendStatus(500);
1827 }
1828});
1829
1830aimlapi.post('/generate-image', async (req, res) => {
1831 try {
1832 const key = readSecret(req.user.directories, SECRET_KEYS.AIMLAPI);
1833 if (!key) return res.sendStatus(400);
1834
1835 console.debug('AI/ML API image request:', req.body);
1836
1837 const apiRes = await fetch('https://api.aimlapi.com/v1/images/generations', {
1838 method: 'POST',
1839 headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${key}`, ...AIMLAPI_HEADERS },
1840 body: JSON.stringify(req.body),
1841 });
1842 if (!apiRes.ok) {
1843 const err = await apiRes.text();
1844 return res.status(500).send(err);
1845 }
1846 /** @type {any} */
1847 const data = await apiRes.json();
1848
1849 const imgObj = Array.isArray(data.images) ? data.images[0] : data.data?.[0];
1850 if (!imgObj) return res.status(500).send('No image returned');
1851
1852 let base64;
1853 if (imgObj.b64_json || imgObj.base64) {
1854 base64 = imgObj.b64_json || imgObj.base64;
1855 } else if (imgObj.url) {
1856 const blobRes = await fetch(imgObj.url);
1857 if (!blobRes.ok) throw new Error('Failed to fetch image URL');
1858 const buffer = await blobRes.arrayBuffer();
1859 base64 = Buffer.from(buffer).toString('base64');
1860 } else {
1861 throw new Error('Unsupported image format');
1862 }
1863
1864 return res.json({ format: 'png', data: base64 });
1865 } catch (e) {
1866 console.error(e);
1867 res.status(500).send('Internal error');
1868 }
1869});
1870
1871const zai = express.Router();
1872
1873zai.post('/generate', async (request, response) => {
1874 try {
1875 const key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
1876
1877 if (!key) {
1878 console.warn('Z.AI key not found.');
1879 return response.sendStatus(400);
1880 }
1881
1882 console.debug('Z.AI image request:', request.body);
1883
1884 // Always use Common API for image generation (Coding API has stricter rate limits)
1885 const generateResponse = await fetch('https://api.z.ai/api/paas/v4/images/generations', {
1886 method: 'POST',
1887 headers: {
1888 'Content-Type': 'application/json',
1889 'Authorization': `Bearer ${key}`,
1890 },
1891 body: JSON.stringify({
1892 prompt: request.body.prompt,
1893 model: request.body.model,
1894 quality: request.body.quality,
1895 size: request.body.size,
1896 }),
1897 });
1898
1899 if (!generateResponse.ok) {
1900 const text = await generateResponse.text();
1901 console.warn('Z.AI returned an error.', text);
1902 return response.sendStatus(500);
1903 }
1904
1905 /** @type {any} */
1906 const data = await generateResponse.json();
1907 console.debug('Z.AI image response:', data);
1908
1909 const urlString = String(data?.data?.[0]?.url ?? '');
1910 if (!urlString || !isValidUrl(urlString)) {
1911 console.warn('Z.AI returned an invalid image URL.');
1912 return response.sendStatus(500);
1913 }
1914
1915 const url = new URL(urlString);
1916 if (!url.hostname.endsWith('.z.ai') && !url.hostname.endsWith('.ufileos.com')) {
1917 console.warn('Z.AI returned a URL with an unrecognized hostname.');
1918 return response.sendStatus(500);
1919 }
1920
1921 for (let attempt = 0; attempt < 5; attempt++) {
1922 const imageResponse = await fetch(url);
1923 if (!imageResponse.ok) {
1924 // Sometimes the URL is valid but the image isn't immediately available
1925 if (imageResponse.status === 404) {
1926 console.info('Z.AI image not found yet, retrying...', { attempt: attempt + 1 });
1927 await delay(1000);
1928 continue;
1929 }
1930
1931 console.warn('Z.AI image fetch returned an error. Status:', imageResponse.status, imageResponse.statusText);
1932 return response.sendStatus(500);
1933 }
1934
1935 const buffer = await imageResponse.arrayBuffer();
1936 const image = Buffer.from(buffer).toString('base64');
1937 const format = path.extname(url.pathname).substring(1).toLowerCase() || 'png';
1938
1939 return response.send({ image, format });
1940 }
1941
1942 console.warn('Z.AI image was not available after multiple attempts.');
1943 return response.sendStatus(500);
1944 } catch (error) {
1945 console.error(error);
1946 return response.sendStatus(500);
1947 }
1948});
1949
1950zai.post('/generate-video', async (request, response) => {
1951 try {
1952 const controller = new AbortController();
1953 request.socket.removeAllListeners('close');
1954 request.socket.on('close', function () {
1955 controller.abort();
1956 });
1957
1958 const key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
1959
1960 if (!key) {
1961 console.warn('Z.AI key not found.');
1962 return response.sendStatus(400);
1963 }
1964
1965 console.debug('Z.AI video request:', request.body);
1966
1967 const generateResponse = await fetch('https://api.z.ai/api/paas/v4/videos/generations', {
1968 method: 'POST',
1969 headers: {
1970 'Content-Type': 'application/json',
1971 'Authorization': `Bearer ${key}`,
1972 },
1973 body: JSON.stringify({
1974 prompt: request.body.prompt,
1975 model: request.body.model,
1976 quality: request.body.quality,
1977 size: request.body.size,
1978 aspect_ratio: request.body.aspect_ratio,
1979 }),
1980 signal: controller.signal,
1981 });
1982
1983 if (!generateResponse.ok) {
1984 const text = await generateResponse.text();
1985 console.warn('Z.AI returned an error.', text);
1986 return response.sendStatus(500);
1987 }
1988
1989 /** @type {any} */
1990 const data = await generateResponse.json();
1991 console.debug('Z.AI video response:', data);
1992
1993 // Poll for video generation completion
1994 for (let attempt = 0; attempt < 30; attempt++) {
1995 if (controller.signal.aborted) {
1996 console.info('Z.AI video generation aborted by client');
1997 return response.status(500).send('Video generation aborted by client');
1998 }
1999
2000 await delay(5000 + attempt * 1000);
2001 console.debug(`Polling Z.AI video job ${data.id}, attempt ${attempt + 1}`);
2002
2003 const pollResponse = await fetch(`https://api.z.ai/api/paas/v4/async-result/${data.id}`, {
2004 method: 'GET',
2005 headers: {
2006 'Authorization': `Bearer ${key}`,
2007 },
2008 });
2009
2010 if (!pollResponse.ok) {
2011 const text = await pollResponse.text();
2012 console.warn('Z.AI video job polling failed', pollResponse.statusText, text);
2013 return response.status(500).send(text);
2014 }
2015
2016 /** @type {any} */
2017 const pollResult = await pollResponse.json();
2018 console.debug(`Z.AI video job status: ${pollResult.task_status}`);
2019
2020 if (pollResult.task_status === 'FAIL') {
2021 console.warn('Z.AI video generation failed', pollResult);
2022 return response.status(500).send('Video generation failed');
2023 }
2024
2025 if (pollResult.task_status === 'SUCCESS') {
2026 console.debug('Z.AI video generation succeeded', pollResult);
2027 const url = pollResult?.video_result?.[0]?.url;
2028
2029 if (!url || !isValidUrl(url)) {
2030 console.warn('Z.AI returned an invalid video URL.');
2031 return response.sendStatus(500);
2032 }
2033
2034 const contentResponse = await fetch(url);
2035 if (!contentResponse.ok) {
2036 const text = await contentResponse.text();
2037 console.warn('Z.AI video content fetch failed', contentResponse.statusText, text);
2038 return response.status(500).send(text);
2039 }
2040
2041 const contentBuffer = await contentResponse.arrayBuffer();
2042 return response.send({ format: 'mp4', video: Buffer.from(contentBuffer).toString('base64') });
2043 }
2044 }
2045 console.warn('Z.AI video was not available after multiple attempts.');
2046 return response.sendStatus(500);
2047 } catch (error) {
2048 console.error(error);
2049 return response.sendStatus(500);
2050 }
2051});
2052
2053const workersai = express.Router();
2054
2055workersai.post('/models', async (request, response) => {
2056 try {
2057 const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
2058
2059 if (!key) {
2060 console.warn('Cloudflare Workers AI API key not found.');
2061 return response.sendStatus(400);
2062 }
2063
2064 const accountId = String(request.body.account_id || '').trim();
2065 if (!accountId) {
2066 console.warn('Cloudflare Workers AI Account ID not found.');
2067 return response.sendStatus(400);
2068 }
2069
2070 const apiUrl = new URL(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/models/search`);
2071 apiUrl.searchParams.set('task', 'Text-to-Image');
2072 apiUrl.searchParams.set('per_page', '1000');
2073 const result = await fetch(apiUrl, {
2074 method: 'GET',
2075 headers: {
2076 'Authorization': `Bearer ${key}`,
2077 },
2078 });
2079
2080 if (!result.ok) {
2081 console.warn('Cloudflare Workers AI returned an error.', result.statusText);
2082 return response.sendStatus(500);
2083 }
2084
2085 /** @type {any} */
2086 const data = await result.json();
2087
2088 if (!data.success || !Array.isArray(data.result)) {
2089 console.warn('Cloudflare Workers AI returned invalid data.');
2090 return response.sendStatus(500);
2091 }
2092
2093 const models = data.result.map(x => ({ value: x.name, text: x.name }));
2094 return response.send(models);
2095 } catch (error) {
2096 console.error(error);
2097 return response.sendStatus(500);
2098 }
2099});
2100
2101workersai.post('/generate', async (request, response) => {
2102 try {
2103 const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
2104
2105 if (!key) {
2106 console.warn('Cloudflare Workers AI API key not found.');
2107 return response.sendStatus(400);
2108 }
2109
2110 const accountId = String(request.body.account_id || '').trim();
2111 if (!accountId) {
2112 console.warn('Cloudflare Workers AI Account ID not found.');
2113 return response.sendStatus(400);
2114 }
2115
2116 const model = String(request.body.model || '').trim();
2117 if (!model) {
2118 console.warn('Cloudflare Workers AI model not specified.');
2119 return response.sendStatus(400);
2120 }
2121
2122 const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`;
2123
2124 const body = {
2125 prompt: request.body.prompt,
2126 negative_prompt: request.body.negative_prompt || undefined,
2127 width: request.body.width ? Number(request.body.width) : undefined,
2128 height: request.body.height ? Number(request.body.height) : undefined,
2129 num_steps: request.body.steps ? Number(request.body.steps) : undefined,
2130 guidance: request.body.scale ? Number(request.body.scale) : undefined,
2131 seed: request.body.seed >= 0 ? Number(request.body.seed) : undefined,
2132 };
2133
2134 // Remove undefined values
2135 for (const prop of Object.keys(body)) {
2136 if (body[prop] === undefined) {
2137 delete body[prop];
2138 }
2139 }
2140
2141 console.debug('Cloudflare Workers AI request:', model, body);
2142
2143 /** @type {import('node-fetch').RequestInit} */
2144 const apiRequest = {
2145 method: 'POST',
2146 headers: {
2147 'Authorization': `Bearer ${key}`,
2148 },
2149 };
2150
2151 if (/flux-2/.test(model)) {
2152 const formData = new FormData();
2153 for (const [key, value] of Object.entries(body)) {
2154 formData.append(key, String(value));
2155 }
2156 apiRequest.body = formData;
2157 } else {
2158 apiRequest.headers = { ...apiRequest.headers, 'Content-Type': 'application/json' };
2159 apiRequest.body = JSON.stringify(body);
2160 }
2161
2162 const result = await fetch(apiUrl, apiRequest);
2163 if (!result.ok) {
2164 const text = await result.text();
2165 console.warn('Cloudflare Workers AI returned an error.', result.status, result.statusText, text);
2166 return response.status(500).send(text);
2167 }
2168
2169 const contentType = result.headers.get('content-type') || '';
2170
2171 // Partner models return JSON with base64 image
2172 if (contentType.includes('application/json')) {
2173 /** @type {any} */
2174 const data = await result.json();
2175 const image = data?.result?.image || data?.image;
2176 if (!image) {
2177 console.warn('Cloudflare Workers AI returned JSON without image data.');
2178 return response.sendStatus(500);
2179 }
2180 return response.send({ format: 'png', image: image });
2181 }
2182
2183 // Non-partner models return raw binary image data
2184 const buffer = await result.arrayBuffer();
2185 return response.send({ format: 'png', image: Buffer.from(buffer).toString('base64') });
2186 } catch (error) {
2187 console.error(error);
2188 return response.sendStatus(500);
2189 }
2190});
2191
2192router.use('/comfy', comfy);
2193router.use('/comfyrunpod', comfyRunPod);
2194router.use('/together', together);
2195router.use('/sdcpp', sdcpp);
2196router.use('/drawthings', drawthings);
2197router.use('/pollinations', pollinations);
2198router.use('/stability', stability);
2199router.use('/huggingface', huggingface);
2200router.use('/chutes', chutes);
2201router.use('/electronhub', electronhub);
2202router.use('/nanogpt', nanogpt);
2203router.use('/bfl', bfl);
2204router.use('/falai', falai);
2205router.use('/xai', xai);
2206router.use('/aimlapi', aimlapi);
2207router.use('/zai', zai);
2208router.use('/workersai', workersai);