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 = {
93 zai: 'zai',93 zai: 'zai',
94 openrouter: 'openrouter',94 openrouter: 'openrouter',
95};95};
96const comfyTypes = {
97 standard: 'standard',
98 runpod_serverless: 'runpod_serverless',
99};
96100
97const initiators = {101const initiators = {
98 command: 'command',102 command: 'command',
@@ -322,9 +326,13 @@ const defaultSettings = {
322 styles: defaultStyles,326 styles: defaultStyles,
323327
324 // ComyUI settings328 // ComyUI settings
329 comfy_type: 'standard',
330
325 comfy_url: 'http://127.0.0.1:8188',331 comfy_url: 'http://127.0.0.1:8188',
326 comfy_workflow: 'Default_Comfy_Workflow.json',332 comfy_workflow: 'Default_Comfy_Workflow.json',
327333
334 comfy_runpod_url: '',
335
328 // Pollinations settings336 // Pollinations settings
329 pollinations_enhance: false,337 pollinations_enhance: false,
330338
@@ -437,6 +445,10 @@ function toggleSourceControls() {
437 const source = $(this).data('sd-source').split(',');445 const source = $(this).data('sd-source').split(',');
438 $(this).toggle(source.includes(extension_settings.sd.source));446 $(this).toggle(source.includes(extension_settings.sd.source));
439 });447 });
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 });
440}452}
441453
442async function loadSettings() {454async function loadSettings() {
@@ -517,8 +529,10 @@ async function loadSettings() {
517 $('#sd_openai_style').val(extension_settings.sd.openai_style);529 $('#sd_openai_style').val(extension_settings.sd.openai_style);
518 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);530 $('#sd_openai_quality').val(extension_settings.sd.openai_quality);
519 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);531 $('#sd_openai_duration').val(extension_settings.sd.openai_duration);
532 $('#sd_comfy_type').val(extension_settings.sd.comfy_type);
520 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);533 $('#sd_comfy_url').val(extension_settings.sd.comfy_url);
521 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);534 $('#sd_comfy_prompt').val(extension_settings.sd.comfy_prompt);
535 $('#sd_comfy_runpod_url').val(extension_settings.sd.comfy_runpod_url);
522 $('#sd_snap').prop('checked', extension_settings.sd.snap);536 $('#sd_snap').prop('checked', extension_settings.sd.snap);
523 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);537 $('#sd_clip_skip').val(extension_settings.sd.clip_skip);
524 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);538 $('#sd_clip_skip_value').val(extension_settings.sd.clip_skip);
@@ -1016,6 +1030,11 @@ async function onSourceChange() {
1016 await loadSettingOptions();1030 await loadSettingOptions();
1017}1031}
10181032
1033async function onComfyTypeChange() {
1034 extension_settings.sd.comfy_type = $('#sd_comfy_type').find(':selected').val();
1035 await onSourceChange();
1036}
1037
1019function onFunctionToolInput() {1038function onFunctionToolInput() {
1020 extension_settings.sd.function_tool = !!$(this).prop('checked');1039 extension_settings.sd.function_tool = !!$(this).prop('checked');
1021 saveSettingsDebounced();1040 saveSettingsDebounced();
@@ -1166,11 +1185,15 @@ function onHrSecondPassStepsInput() {
1166}1185}
11671186
1168function onComfyUrlInput() {1187function onComfyUrlInput() {
1169 // Remove trailing slashes
1170 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());1188 extension_settings.sd.comfy_url = String($('#sd_comfy_url').val());
1171 saveSettingsDebounced();1189 saveSettingsDebounced();
1172}1190}
11731191
1192function onComfyRunPodUrlInput() {
1193 extension_settings.sd.comfy_runpod_url = String($('#sd_comfy_runpod_url').val());
1194 saveSettingsDebounced();
1195}
1196
1174function onHFModelInput() {1197function onHFModelInput() {
1175 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();1198 extension_settings.sd.huggingface_model_id = $('#sd_huggingface_model_id').val();
1176 saveSettingsDebounced();1199 saveSettingsDebounced();
@@ -1296,6 +1319,30 @@ async function validateComfyUrl() {
1296 }1319 }
1297}1320}
12981321
1322async 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
1299async function onModelChange() {1346async function onModelChange() {
1300 const selectedModel = $('#sd_model').find(':selected');1347 const selectedModel = $('#sd_model').find(':selected');
1301 extension_settings.sd.model = selectedModel.val();1348 extension_settings.sd.model = selectedModel.val();
@@ -1309,27 +1356,13 @@ async function onModelChange() {
13091356
1310 switchModelSpecificControls(extension_settings.sd.model);1357 switchModelSpecificControls(extension_settings.sd.model);
13111358
1312 const cloudSources = [1359 const updateRemoteModelSources = [
1313 sources.horde,1360 sources.auto,
1314 sources.novel,1361 sources.vlad,
1315 sources.openai,1362 sources.extras,
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,
1330 ];1363 ];
13311364
1332 if (cloudSources.includes(extension_settings.sd.source)) {1365 if (!updateRemoteModelSources.includes(extension_settings.sd.source)) {
1333 return;1366 return;
1334 }1367 }
13351368
@@ -1685,6 +1718,9 @@ async function loadNovelSamplers() {
1685}1718}
16861719
1687async function loadComfySamplers() {1720async function loadComfySamplers() {
1721 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
1722 return ['N/A'];
1723 }
1688 if (!extension_settings.sd.comfy_url) {1724 if (!extension_settings.sd.comfy_url) {
1689 return [];1725 return [];
1690 }1726 }
@@ -2279,6 +2315,12 @@ function loadNovelSchedulers() {
2279}2315}
22802316
2281async function loadComfyModels() {2317async 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 }
2282 if (!extension_settings.sd.comfy_url) {2324 if (!extension_settings.sd.comfy_url) {
2283 return [];2325 return [];
2284 }2326 }
@@ -2388,6 +2430,9 @@ async function loadSchedulers() {
2388}2430}
23892431
2390async function loadComfySchedulers() {2432async function loadComfySchedulers() {
2433 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2434 return ['N/A'];
2435 }
2391 if (!extension_settings.sd.comfy_url) {2436 if (!extension_settings.sd.comfy_url) {
2392 return [];2437 return [];
2393 }2438 }
@@ -2521,6 +2566,9 @@ async function loadAutoVaes() {
2521}2566}
25222567
2523async function loadComfyVaes() {2568async function loadComfyVaes() {
2569 if (extension_settings.sd.comfy_type === comfyTypes.runpod_serverless) {
2570 return ['N/A'];
2571 }
2524 if (!extension_settings.sd.comfy_url) {2572 if (!extension_settings.sd.comfy_url) {
2525 return [];2573 return [];
2526 }2574 }
@@ -2543,10 +2591,6 @@ async function loadComfyVaes() {
2543}2591}
25442592
2545async function loadComfyWorkflows() {2593async function loadComfyWorkflows() {
2546 if (!extension_settings.sd.comfy_url) {
2547 return;
2548 }
2549
2550 try {2594 try {
2551 $('#sd_comfy_workflow').empty();2595 $('#sd_comfy_workflow').empty();
2552 const result = await fetch('/api/sd/comfy/workflows', {2596 const result = await fetch('/api/sd/comfy/workflows', {
@@ -3040,7 +3084,16 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
3040 result = await generateAimlapiImage(prefixedPrompt, signal);3084 result = await generateAimlapiImage(prefixedPrompt, signal);
3041 break;3085 break;
3042 case sources.comfy:3086 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 }
3044 break;3097 break;
3045 case sources.togetherai:3098 case sources.togetherai:
3046 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);3099 result = await generateTogetherAIImage(prefixedPrompt, negativePrompt, signal);
@@ -3775,25 +3828,17 @@ async function generateAimlapiImage(prompt, signal) {
3775}3828}
37763829
3777/**3830/**
3778 * Generates an image in ComfyUI using the provided prompt and configuration settings.3831 * Generates an image in local ComfyUI or serverless runpod using the provided prompt and configuration settings.
3779 *3832 *
3780 * @param {string} prompt - The main instruction used to guide the image generation.3833 * @param {string} prompt - The main instruction used to guide the image generation.
3781 * @param {string} negativePrompt - The instruction used to restrict the image generation.3834 * @param {string} negativePrompt - The instruction used to restrict the image generation.
3782 * @param {AbortSignal} signal - An AbortSignal object that can be used to cancel the request.3835 * @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.
3783 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.3839 * @returns {Promise<{format: string, data: string}>} - A promise that resolves when the image generation and processing are complete.
3784 */3840 */
3785async function generateComfyImage(prompt, negativePrompt, signal) {3841async function generateComfyImageCommon(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
3797 const workflowResponse = await fetch('/api/sd/comfy/workflow', {3842 const workflowResponse = await fetch('/api/sd/comfy/workflow', {
3798 method: 'POST',3843 method: 'POST',
3799 headers: getRequestHeaders(),3844 headers: getRequestHeaders(),
@@ -3848,12 +3893,12 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3848 console.log(`{3893 console.log(`{
3849 "prompt": ${workflow}3894 "prompt": ${workflow}
3850 }`);3895 }`);
3851 const promptResult = await fetch('/api/sd/comfy/generate', {3896 const promptResult = await fetch(`${basePath}/generate`, {
3852 method: 'POST',3897 method: 'POST',
3853 headers: getRequestHeaders(),3898 headers: getRequestHeaders(),
3854 signal: signal,3899 signal: signal,
3855 body: JSON.stringify({3900 body: JSON.stringify({
3856 url: extension_settings.sd.comfy_url,3901 url,
3857 prompt: `{3902 prompt: `{
3858 "prompt": ${workflow}3903 "prompt": ${workflow}
3859 }`,3904 }`,
@@ -3867,6 +3912,46 @@ async function generateComfyImage(prompt, negativePrompt, signal) {
3867 return { format, data };3912 return { format, data };
3868}3913}
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 */
3923async 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 */
3945async 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
3871/**3956/**
3872 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).3957 * Generates an image in Hugging Face Inference API using the provided prompt and configuration settings (model selected).
@@ -4501,7 +4586,15 @@ function isValidState() {
4501 case sources.aimlapi:4586 case sources.aimlapi:
4502 return secret_state[SECRET_KEYS.AIMLAPI];4587 return secret_state[SECRET_KEYS.AIMLAPI];
4503 case sources.comfy:4588 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 }
4505 case sources.togetherai:4598 case sources.togetherai:
4506 return secret_state[SECRET_KEYS.TOGETHERAI];4599 return secret_state[SECRET_KEYS.TOGETHERAI];
4507 case sources.pollinations:4600 case sources.pollinations:
@@ -5167,8 +5260,11 @@ jQuery(async () => {
5167 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);5260 $('#sd_novel_decrisper').on('input', onNovelDecrisperInput);
5168 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);5261 $('#sd_novel_variety_boost').on('input', onNovelVarietyBoostInput);
5169 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);5262 $('#sd_pollinations_enhance').on('input', onPollinationsEnhanceInput);
5263 $('#sd_comfy_type').on('change', onComfyTypeChange);
5170 $('#sd_comfy_validate').on('click', validateComfyUrl);5264 $('#sd_comfy_validate').on('click', validateComfyUrl);
5265 $('#sd_comfy_runpod_validate').on('click', validateComfyRunPodUrl);
5171 $('#sd_comfy_url').on('input', onComfyUrlInput);5266 $('#sd_comfy_url').on('input', onComfyUrlInput);
5267 $('#sd_comfy_runpod_url').on('input', onComfyRunPodUrlInput);
5172 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);5268 $('#sd_comfy_workflow').on('change', onComfyWorkflowChange);
5173 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);5269 $('#sd_comfy_open_workflow_editor').on('click', onComfyOpenWorkflowEditorClick);
5174 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);5270 $('#sd_comfy_new_workflow').on('click', onComfyNewWorkflowClick);
@@ -5246,6 +5342,7 @@ jQuery(async () => {
5246 case SECRET_KEYS.FALAI:5342 case SECRET_KEYS.FALAI:
5247 case SECRET_KEYS.STABILITY:5343 case SECRET_KEYS.STABILITY:
5248 case SECRET_KEYS.AIMLAPI:5344 case SECRET_KEYS.AIMLAPI:
5345 case SECRET_KEYS.COMFY_RUNPOD:
5249 await loadSettingOptions();5346 await loadSettingOptions();
5250 break;5347 break;
5251 }5348 }
public/scripts/extensions/stable-diffusion/settings.html+38 -8
@@ -198,14 +198,44 @@
198 </div>198 </div>
199 </div>199 </div>
200 <div data-sd-source="comfy">200 <div data-sd-source="comfy">
201 <label for="sd_comfy_url">ComfyUI URL</label>201 <label for="sd_comfy_type">Server Type</label>
202 <div class="flex-container flexnowrap">202 <select id="sd_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>
206 <span data-i18n="Connect">206 <div data-sd-comfy-type="standard">
207 Connect207 <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>
209 </div>239 </div>
210 </div>240 </div>
211 <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>241 <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 = {
57 NANOGPT: 'api_key_nanogpt',57 NANOGPT: 'api_key_nanogpt',
58 TAVILY: 'api_key_tavily',58 TAVILY: 'api_key_tavily',
59 BFL: 'api_key_bfl',59 BFL: 'api_key_bfl',
60 COMFY_RUNPOD: 'api_key_comfy_runpod',
60 GENERIC: 'api_key_generic',61 GENERIC: 'api_key_generic',
61 DEEPSEEK: 'api_key_deepseek',62 DEEPSEEK: 'api_key_deepseek',
62 SERPER: 'api_key_serper',63 SERPER: 'api_key_serper',
@@ -112,6 +113,7 @@ const FRIENDLY_NAMES = {
112 [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS',113 [SECRET_KEYS.CUSTOM_OPENAI_TTS]: 'Custom OpenAI TTS',
113 [SECRET_KEYS.TAVILY]: 'Tavily',114 [SECRET_KEYS.TAVILY]: 'Tavily',
114 [SECRET_KEYS.BFL]: 'Black Forest Labs',115 [SECRET_KEYS.BFL]: 'Black Forest Labs',
116 [SECRET_KEYS.COMFY_RUNPOD]: 'ComfyUI RunPod',
115 [SECRET_KEYS.SERPAPI]: 'SerpApi',117 [SECRET_KEYS.SERPAPI]: 'SerpApi',
116 [SECRET_KEYS.SERPER]: 'Serper',118 [SECRET_KEYS.SERPER]: 'Serper',
117 [SECRET_KEYS.FALAI]: 'FAL.AI',119 [SECRET_KEYS.FALAI]: 'FAL.AI',
@@ -174,6 +176,7 @@ const INPUT_MAP = {
174 [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',176 [SECRET_KEYS.AZURE_OPENAI]: '#api_key_azure_openai',
175 [SECRET_KEYS.ZAI]: '#api_key_zai',177 [SECRET_KEYS.ZAI]: '#api_key_zai',
176 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',178 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
179 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
177};180};
178181
179const getLabel = () => moment().format('L LT');182const getLabel = () => moment().format('L LT');
src/endpoints/secrets.js+1 -0
@@ -49,6 +49,7 @@ export const SECRET_KEYS = {
49 ELECTRONHUB: 'api_key_electronhub',49 ELECTRONHUB: 'api_key_electronhub',
50 NANOGPT: 'api_key_nanogpt',50 NANOGPT: 'api_key_nanogpt',
51 BFL: 'api_key_bfl',51 BFL: 'api_key_bfl',
52 COMFY_RUNPOD: 'api_key_comfy_runpod',
52 FALAI: 'api_key_falai',53 FALAI: 'api_key_falai',
53 GENERIC: 'api_key_generic',54 GENERIC: 'api_key_generic',
54 DEEPSEEK: 'api_key_deepseek',55 DEEPSEEK: 'api_key_deepseek',
src/endpoints/stable-diffusion.js+103 -0
@@ -603,6 +603,108 @@ comfy.post('/generate', async (request, response) => {
603 }603 }
604});604});
605605
606const comfyRunPod = express.Router();
607
608comfyRunPod.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
639comfyRunPod.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
606const together = express.Router();708const together = express.Router();
607709
608together.post('/models', async (request, response) => {710together.post('/models', async (request, response) => {
@@ -1704,6 +1806,7 @@ zai.post('/generate', async (request, response) => {
1704});1806});
17051807
1706router.use('/comfy', comfy);1808router.use('/comfy', comfy);
1809router.use('/comfyrunpod', comfyRunPod);
1707router.use('/together', together);1810router.use('/together', together);
1708router.use('/drawthings', drawthings);1811router.use('/drawthings', drawthings);
1709router.use('/pollinations', pollinations);1812router.use('/pollinations', pollinations);