Comfyui serverless runpod image generation (#4891) * Add comfyrunpod generation endpoints. Get a bunch of settings added. Probably get runpod api secret stored properly. sending to runpod. Correct workflow wrapping for runpod. * Reuse comfyui workflow setting block for runpod. * Update API key url to point to runpod settings * Remove debug print. * Combine normal and runpod comfyui generation functions * Remove unused server endpoints for runpod. * Remove debug prints from server. * Remove redundant switch case. * Check for url to see if image generation is possible for comfy and comfy runpod. * lint * Correct/update doc comments. * Make runpod serverless a sub-type under comfyui source in the UI. * Set comfy_type in stable-diffusion defaultSettings * Allow loading comfy workflows even if url isnt set yet. * Change default runpod url to template example * Rename runpod api key to comfy_runpod * Handle default switches separately from standard now that default setting is ensured. * Update runpod api key name on server. * Move example runpod url to placeholder. * lint * Remove incorrect comment. * Hide toasts remote models toasts * React to key updates * Add type annotation for data and improve workflow handling in generate endpoint --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

3098c4c5bff43640f4fb8baaaad0ae75d634053d

9nbf7c4q6b-lgtm <9nbf7c4q6b@privaterelay.appleid.com>

Signed
5 files changed, +283 -49Ignore whitespace
public/scripts/extensions/stable-diffusion/index.js+138 -41
@@ -93,6 +93,10 @@ const sources = {
9393 zai: 'zai',
9494 openrouter: 'openrouter',
9595};
96+const comfyTypes = {
97+ standard: 'standard',
98+ runpod_serverless: 'runpod_serverless',
99+};
96100
97101const initiators = {
98102 command: 'command',
@@ -322,9 +326,13 @@ const defaultSettings = {
322326 styles: defaultStyles,
323327
324328 // ComyUI settings
329+ comfy_type: 'standard',
330+
325331 comfy_url: 'http://127.0.0.1:8188',
326332 comfy_workflow: 'Default_Comfy_Workflow.json',
327333
334+ comfy_runpod_url: '',
335+
328336 // Pollinations settings
329337 pollinations_enhance: false,
330338
@@ -437,6 +445,10 @@ function toggleSourceControls() {
437445 const source = $(this).data('sd-source').split(',');
438446 $(this).toggle(source.includes(extension_settings.sd.source));
439447 });
448+ $('.sd_settings [data-sd-comfy-type]').each(function () {
449+ const source = $(this).data('sd-comfy-type').split(',');
450+ $(this).toggle(source.includes(extension_settings.sd.comfy_type));
451+ });
440452}
441453
442454async function loadSettings() {
@@ -517,8 +529,10 @@ async function loadSettings() {
517529 $('#sd_openai_style').val(extension_settings.sd.openai_style);
518530 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);
519531 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);
532+ $('#sd_comfy_type').val(extension_settings.sd.comfy_type);
520533 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);
521534 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
535+ $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);
522536 $('#sd_snap').prop('checked', extension_settings.sd.snap);
523537 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
524538 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
@@ -1016,6 +1030,11 @@ async function onSourceChange() {
10161030 await loadSettingOptions();
10171031}
10181032
1033+async function onComfyTypeChange() {
1034+ extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val();
1035+ await onSourceChange();
1036+}
1037+
10191038function onFunctionToolInput() {
10201039 extension_settings.sd.function_tool = !!$(this).prop('checked');
10211040 saveSettingsDebounced();
@@ -1166,11 +1185,15 @@ function onHrSecondPassStepsInput() {
11661185}
11671186
11681187function onComfyUrlInput() {
1169- // Remove trailing slashes
11701188 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());
11711189 saveSettingsDebounced();
11721190}
11731191
1192+function onComfyRunPodUrlInput() {
1193+ extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val());
1194+ saveSettingsDebounced();
1195+}
1196+
11741197function onHFModelInput() {
11751198 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
11761199 saveSettingsDebounced();
@@ -1296,6 +1319,30 @@ async function validateComfyUrl() {
12961319 }
12971320}
12981321
1322+async function validateComfyRunPodUrl() {
1323+ try {
1324+ if (!extension_settings.sd.comfy_runpod_url) {
1325+ throw new Error('URL is not set.');
1326+ }
1327+
1328+ const result = await fetch('/api/sd/comfyrunpod/ping', {
1329+ method: 'POST',
1330+ headers: getRequestHeaders(),
1331+ body: JSON.stringify({
1332+ url: extension_settings.sd.comfy_runpod_url,
1333+ }),
1334+ });
1335+ if (!result.ok) {
1336+ throw new Error('ComfyUI RunPod returned an error.');
1337+ }
1338+
1339+ await loadSettingOptions();
1340+ toastr.success('ComfyUI RunPod API connected.');
1341+ } catch (error) {
1342+ toastr.error(`Could not validate ComfyUI RunPod API: ${error.message}`);
1343+ }
1344+}
1345+
12991346async function onModelChange() {
13001347 const selectedModel = $('#sd_model').find(':selected');
13011348 extension_settings.sd.model = selectedModel.val();
@@ -1309,27 +1356,13 @@ async function onModelChange() {
13091356
13101357 switchModelSpecificControls(extension_settings.sd.model);
13111358
13121359 const cloudSourcesupdateRemoteModelSources = [
13131360 sources.hordeauto,
13141361 sources.novelvlad,
13151362 sources.openaiextras,
1316- sources.aimlapi,
1317- sources.togetherai,
1318- sources.pollinations,
1319- sources.stability,
1320- sources.huggingface,
1321- sources.electronhub,
1322- sources.nanogpt,
1323- sources.bfl,
1324- sources.falai,
1325- sources.xai,
1326- sources.google,
1327- sources.chutes,
1328- sources.zai,
1329- sources.openrouter,
13301363 ];
13311364
13321365 if (cloudSources!updateRemoteModelSources.includes(extension_settings.sd.source)) {
13331366 return;
13341367 }
13351368
@@ -1685,6 +1718,9 @@ async function loadNovelSamplers() {
16851718}
16861719
16871720async function loadComfySamplers() {
1721+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
1722+ return ['N/A'];
1723+ }
16881724 if (!extension_settings.sd.comfy_url) {
16891725 return [];
16901726 }
@@ -2279,6 +2315,12 @@ function loadNovelSchedulers() {
22792315}
22802316
22812317async function loadComfyModels() {
2318+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2319+ $('#sd_runpod_key').toggleClass('success', !!secret_state[SECRET_KEYS.COMFY_RUNPOD]);
2320+ return [
2321+ { value: '', text: 'N/A' },
2322+ ];
2323+ }
22822324 if (!extension_settings.sd.comfy_url) {
22832325 return [];
22842326 }
@@ -2388,6 +2430,9 @@ async function loadSchedulers() {
23882430}
23892431
23902432async function loadComfySchedulers() {
2433+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2434+ return ['N/A'];
2435+ }
23912436 if (!extension_settings.sd.comfy_url) {
23922437 return [];
23932438 }
@@ -2521,6 +2566,9 @@ async function loadAutoVaes() {
25212566}
25222567
25232568async function loadComfyVaes() {
2569+ if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2570+ return ['N/A'];
2571+ }
25242572 if (!extension_settings.sd.comfy_url) {
25252573 return [];
25262574 }
@@ -2543,10 +2591,6 @@ async function loadComfyVaes() {
25432591}
25442592
25452593async function loadComfyWorkflows() {
2546- if (!extension_settings.sd.comfy_url) {
2547- return;
2548- }
2549-
25502594 try {
25512595 $('#sd_comfy_workflow').empty();
25522596 const result = await fetch('/api/sd/comfy/workflows', {
@@ -3040,7 +3084,16 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
30403084 result = await generateAimlapiImage(prefixedPrompt, signal);
30413085 break;
30423086 case sources.comfy:
3043- result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
3087+ switch (extension_settings.sd.comfy_type) {
3088+ case comfyTypes.runpod_serverless:
3089+ result = await generateComfyRunPodImage(prefixedPrompt, negativePrompt, signal);
3090+ break;
3091+ case comfyTypes.standard:
3092+ result = await generateComfyImage(prefixedPrompt, negativePrompt, signal);
3093+ break;
3094+ default:
3095+ throw new Error('Unknown comfyUI server type.');
3096+ }
30443097 break;
30453098 case sources.togetherai:
30463099 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);
@@ -3775,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) {
37753828}
37763829
37773830/**
37783831 * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings.
37793832 *
37803833 * @param {string} prompt - The main instruction used to guide the image generation.
37813834 * @param {string} negativePrompt - The instruction used to restrict the image generation.
37823835 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3836+ * @param {string} basePath - ST server endpoint for the service. '/api/sd/comfy' for local, '/api/sd/comfyrunpod' for serverless.
3837+ * @param {string[]} placeholders - Array of substitutions to apply to the workflow.
3838+ * @param {string} url - The url of the service to call. Passed to ST server.
37833839 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
37843840 */
37853841async function generateComfyImagegenerateComfyImageCommon(prompt, negativePrompt, signal, basePath, placeholders, url) {
3786- const placeholders = [
3787- 'model',
3788- 'vae',
3789- 'sampler',
3790- 'scheduler',
3791- 'steps',
3792- 'scale',
3793- 'width',
3794- 'height',
3795- ];
3796-
37973842 const workflowResponse = await fetch('/api/sd/comfy/workflow', {
37983843 method: 'POST',
37993844 headers: getRequestHeaders(),
@@ -3848,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
38483893 console.log(`{
38493894 "prompt": ${workflow}
38503895 }`);
38513896 const promptResult = await fetch('/api/sd/comfy`${basePath}/generate'`, {
38523897 method: 'POST',
38533898 headers: getRequestHeaders(),
38543899 signal: signal,
38553900 body: JSON.stringify({
3856- url: extension_settings.sd.comfy_url,
3901+ url,
38573902 prompt: `{
38583903 "prompt": ${workflow}
38593904 }`,
@@ -3867,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
38673912 return { format, data };
38683913}
38693914
3915+/**
3916+ * Generates an image in ComfyUI using the provided prompt and configuration settings.
3917+ *
3918+ * @param {string} prompt - The main instruction used to guide the image generation.
3919+ * @param {string} negativePrompt - The instruction used to restrict the image generation.
3920+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3921+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3922+ */
3923+async function generateComfyImage(prompt, negativePrompt, signal) {
3924+ const placeholders = [
3925+ 'model',
3926+ 'vae',
3927+ 'sampler',
3928+ 'scheduler',
3929+ 'steps',
3930+ 'scale',
3931+ 'width',
3932+ 'height',
3933+ ];
3934+ return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfy', placeholders, extension_settings.sd.comfy_url);
3935+}
3936+
3937+/**
3938+ * Generates an image using ComfyUI through serverless runpod endpoint using the provided prompt and configuration settings.
3939+ *
3940+ * @param {string} prompt - The main instruction used to guide the image generation.
3941+ * @param {string} negativePrompt - The instruction used to restrict the image generation.
3942+ * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.
3943+ * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3944+ */
3945+async function generateComfyRunPodImage(prompt, negativePrompt, signal) {
3946+ const placeholders = [
3947+ 'steps',
3948+ 'scale',
3949+ 'width',
3950+ 'height',
3951+ ];
3952+
3953+ return generateComfyImageCommon(prompt, negativePrompt, signal, '/api/sd/comfyrunpod', placeholders, extension_settings.sd.comfy_runpod_url);
3954+}
38703955
38713956/**
38723957 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).
@@ -4501,7 +4586,15 @@ function isValidState() {
45014586 case sources.aimlapi:
45024587 return secret_state[SECRET_KEYS.AIMLAPI];
45034588 case sources.comfy:
4504- return true;
4589+ switch (extension_settings.sd.comfy_type) {
4590+ case comfyTypes.runpod_serverless:
4591+ return !!extension_settings.sd.comfy_runpod_url &&
4592+ secret_state[SECRET_KEYS.COMFY_RUNPOD];
4593+ case comfyTypes.standard:
4594+ return !!extension_settings.sd.comfy_url;
4595+ default:
4596+ return false;
4597+ }
45054598 case sources.togetherai:
45064599 return secret_state[SECRET_KEYS.TOGETHERAI];
45074600 case sources.pollinations:
@@ -5167,8 +5260,11 @@ jQuery(async () => {
51675260 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
51685261 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);
51695262 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
5263+ $('#sd_comfy_type').on('change', onComfyTypeChange);
51705264 $('#sd_comfy_validate').on('click', validateComfyUrl);
5265+ $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl);
51715266 $('#sd_comfy_url').on('input', onComfyUrlInput);
5267+ $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput);
51725268 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
51735269 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
51745270 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
@@ -5246,6 +5342,7 @@ jQuery(async () => {
52465342 case SECRET_KEYS.FALAI:
52475343 case SECRET_KEYS.STABILITY:
52485344 case SECRET_KEYS.AIMLAPI:
5345+ case SECRET_KEYS.COMFY_RUNPOD:
52495346 await loadSettingOptions();
52505347 break;
52515348 }
public/scripts/extensions/stable-diffusion/settings.html+38 -8
@@ -198,14 +198,44 @@
198198 </div>
199199 </div>
200200 <div data-sd-source="comfy">
201201 <label for="sd_comfy_urlsd_comfy_type">ComfyUIServer URLType</label>
202202 <divselect classid="flex-container flexnowrapsd_comfy_type">
203- <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />
203+ <option value="standard">Standard Server</option>
204- <div id="sd_comfy_validate" class="menu_button menu_button_icon">
204+ <option value="runpod_serverless">RunPod Serverless Endpoint</option>
205- <i class="fa-solid fa-check"></i>
205+ </select>
206206 <spandiv data-i18nsd-comfy-type="Connectstandard">
207- Connect
207+ <label for="sd_comfy_url">ComfyUI URL</label>
208- </span>
208+ <div class="flex-container flexnowrap">
209+ <input id="sd_comfy_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_url" placeholder="Example: {{comfy_url}}" value="{{comfy_url}}" />
210+ <div id="sd_comfy_validate" class="menu_button menu_button_icon">
211+ <i class="fa-solid fa-check"></i>
212+ <span data-i18n="Connect">
213+ Connect
214+ </span>
215+ </div>
216+ </div>
217+ </div>
218+ <div data-sd-comfy-type="runpod_serverless">
219+ <label for="sd_comfy_runpod_url">ComfyUI RunPod URL</label>
220+ <div class="flex-container flexnowrap">
221+ <input id="sd_comfy_runpod_url" type="text" class="text_pole" data-i18n="[placeholder]sd_comfy_runpod_url" placeholder="eg: https://api.runpod.ai/v2/<your endpoint id>" value="{{comfy_runpod_url}}" />
222+ <div id="sd_comfy_runpod_validate" class="menu_button menu_button_icon">
223+ <i class="fa-solid fa-check"></i>
224+ <span data-i18n="Connect">
225+ Connect
226+ </span>
227+ </div>
228+ </div>
229+ <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
230+ <a href="https://console.runpod.io/user/settings" target="_blank" rel="noopener noreferrer">
231+ <strong data-i18n="API Key">API Key</strong>
232+ <i class="fa-solid fa-share-from-square"></i>
233+ </a>
234+ <span class="expander"></span>
235+ <div id="sd_runpod_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_comfy_runpod">
236+ <i class="fa-fw fa-solid fa-key"></i>
237+ <span data-i18n="Click to set">Click to set</span>
238+ </div>
209239 </div>
210240 </div>
211241 <p><i><b data-i18n="Important:">Important:</b></i><i data-i18n="The server must be accessible from the SillyTavern host machine."> The server must be accessible from the SillyTavern host machine.</i></p>
public/scripts/secrets.js+3 -0
@@ -57,6 +57,7 @@ export const SECRET_KEYS = {
5757 NANOGPT: 'api_key_nanogpt',
5858 TAVILY: 'api_key_tavily',
5959 BFL: 'api_key_bfl',
60+ COMFY_RUNPOD: 'api_key_comfy_runpod',
6061 GENERIC: 'api_key_generic',
6162 DEEPSEEK: 'api_key_deepseek',
6263 SERPER: 'api_key_serper',
@@ -112,6 +113,7 @@ const FRIENDLY_NAMES = {
112113 [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS',
113114 [SECRET_KEYS.TAVILY]: 'Tavily',
114115 [SECRET_KEYS.BFL]: 'Black Forest Labs',
116+ [SECRET_KEYS.COMFY_RUNPOD]: 'ComfyUI RunPod',
115117 [SECRET_KEYS.SERPAPI]: 'SerpApi',
116118 [SECRET_KEYS.SERPER]: 'Serper',
117119 [SECRET_KEYS.FALAI]: 'FAL.AI',
@@ -174,6 +176,7 @@ const INPUT_MAP = {
174176 [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',
175177 [SECRET_KEYS.ZAI]: '#api_key_zai',
176178 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
179+ [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
177180};
178181
179182const getLabel = () => moment().format('L LT');
src/endpoints/secrets.js+1 -0
@@ -49,6 +49,7 @@ export const SECRET_KEYS = {
4949 ELECTRONHUB: 'api_key_electronhub',
5050 NANOGPT: 'api_key_nanogpt',
5151 BFL: 'api_key_bfl',
52+ COMFY_RUNPOD: 'api_key_comfy_runpod',
5253 FALAI: 'api_key_falai',
5354 GENERIC: 'api_key_generic',
5455 DEEPSEEK: 'api_key_deepseek',
src/endpoints/stable-diffusion.js+103 -0
@@ -603,6 +603,108 @@ comfy.post('/generate', async (request, response) => {
603603 }
604604});
605605
606+const comfyRunPod = express.Router();
607+
608+comfyRunPod.post('/ping', async (request, response) => {
609+ try {
610+ const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD);
611+
612+ if (!key) {
613+ console.warn('RunPod key not found.');
614+ return response.sendStatus(400);
615+ }
616+
617+ const url = new URL(urlJoin(request.body.url, '/health'));
618+
619+ const result = await fetch(url, {
620+ method: 'GET',
621+ headers: { 'Authorization': `Bearer ${key}` },
622+ });
623+ if (!result.ok) {
624+ throw new Error('ComfyUI returned an error.');
625+ }
626+ /** @type {any} */
627+ const data = await result.json();
628+ if (data.workers.ready <= 0) {
629+ console.warn(`No workers reported as ready. ${result}`);
630+ }
631+
632+ return response.sendStatus(200);
633+ } catch (error) {
634+ console.error(error);
635+ return response.sendStatus(500);
636+ }
637+});
638+
639+comfyRunPod.post('/generate', async (request, response) => {
640+ try {
641+ const key = readSecret(request.user.directories, SECRET_KEYS.COMFY_RUNPOD);
642+
643+ if (!key) {
644+ console.warn('RunPod key not found.');
645+ return response.sendStatus(400);
646+ }
647+
648+ let jobId;
649+ let item;
650+ const url = new URL(urlJoin(request.body.url, '/run'));
651+
652+ const controller = new AbortController();
653+ request.socket.removeAllListeners('close');
654+ request.socket.on('close', function () {
655+ if (!response.writableEnded && !item) {
656+ const interruptUrl = new URL(urlJoin(request.body.url, `/cancel/${jobId}`));
657+ fetch(interruptUrl, { method: 'POST', headers: { 'Authorization': `Bearer ${key}` } });
658+ }
659+ controller.abort();
660+ });
661+ const workflow = JSON.parse(request.body.prompt).prompt;
662+ const wrappedWorkflow = workflow?.input?.workflow ? workflow : ({ input: { workflow: workflow } });
663+ const runpodPrompt = JSON.stringify(wrappedWorkflow);
664+
665+ console.debug('ComfyUI RunPod request:', wrappedWorkflow);
666+
667+ const promptResult = await fetch(url, {
668+ method: 'POST',
669+ headers: { 'Authorization': `Bearer ${key}` },
670+ body: runpodPrompt,
671+ });
672+ if (!promptResult.ok) {
673+ const text = await promptResult.text();
674+ throw new Error('ComfyUI returned an error.', { cause: tryParse(text) });
675+ }
676+
677+ /** @type {any} */
678+ const data = await promptResult.json();
679+ jobId = data.id;
680+ const statusUrl = new URL(urlJoin(request.body.url, `/status/${jobId}`));
681+ while (true) {
682+ const result = await fetch(statusUrl, {
683+ method: 'GET',
684+ headers: { 'Authorization': `Bearer ${key}` },
685+ });
686+ if (!result.ok) {
687+ throw new Error('ComfyUI returned an error.');
688+ }
689+ /** @type {any} */
690+ const status = await result.json();
691+ if (status.output) {
692+ item = status.output.images[0];
693+ }
694+ if (item) {
695+ break;
696+ }
697+ await delay(500);
698+ }
699+ const format = path.extname(item.filename).slice(1).toLowerCase() || 'png';
700+ return response.send({ format: format, data: item.data });
701+ } catch (error) {
702+ console.error('ComfyUI error:', error);
703+ response.status(500).send(error.message);
704+ return response;
705+ }
706+});
707+
606708const together = express.Router();
607709
608710together.post('/models', async (request, response) => {
@@ -1704,6 +1806,7 @@ zai.post('/generate', async (request, response) => {
17041806});
17051807
17061808router.use('/comfy', comfy);
1809+router.use('/comfyrunpod', comfyRunPod);
17071810router.use('/together', together);
17081811router.use('/drawthings', drawthings);
17091812router.use('/pollinations', pollinations);