Integrate Cloudflare Workers AI text-to-image into SD extension (#5434) * feat: integrate Cloudflare Workers AI for text-to-image generation in SD extension Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/efc79e4d-2119-4cdb-8afb-f26e318a38ef Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: address review - use oai_settings for account ID, sort dropdown alphabetically, remove Account ID input, move debug log Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/bf0dda38-df40-44f4-8a63-0c952b48905d Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Clean-up diffs * feat: add refresh models button to Workers AI section Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/ab6b5e7a-84d2-44d1-9f6e-3d330de04ef1 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: revert unrelated package-lock.json changes Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/ab6b5e7a-84d2-44d1-9f6e-3d330de04ef1 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Fix models loading * refactor: update model refresh button ID and add class to select elements * Send formData to BFL models * fix: adjust use FormData condition * fix: validate Workers AI account ID before proceeding with image model loading --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>

78628f7dbb7c2f0d1cb33cac655994ab87a91903

Copilot <198982749+Copilot@users.noreply.github.com>

Signed
4 files changed, +252 -21Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+76 -0
@@ -99,6 +99,7 @@ const sources = {
99 google: 'google',99 google: 'google',
100 zai: 'zai',100 zai: 'zai',
101 openrouter: 'openrouter',101 openrouter: 'openrouter',
102 workersai: 'workersai',
102};103};
103const comfyTypes = {104const comfyTypes = {
104 standard: 'standard',105 standard: 'standard',
@@ -1747,6 +1748,9 @@ async function loadSamplers() {
1747 case sources.openrouter:1748 case sources.openrouter:
1748 samplers = ['N/A'];1749 samplers = ['N/A'];
1749 break;1750 break;
1751 case sources.workersai:
1752 samplers = ['N/A'];
1753 break;
1750 }1754 }
17511755
1752 for (const sampler of samplers) {1756 for (const sampler of samplers) {
@@ -1997,6 +2001,9 @@ async function loadModels() {
1997 case sources.openrouter:2001 case sources.openrouter:
1998 models = await loadOpenRouterModels();2002 models = await loadOpenRouterModels();
1999 break;2003 break;
2004 case sources.workersai:
2005 models = await loadWorkersAIImageModels();
2006 break;
2000 }2007 }
20012008
2002 if (extension_settings.sd.source === sources.electronhub) {2009 if (extension_settings.sd.source === sources.electronhub) {
@@ -2124,6 +2131,33 @@ async function loadXAIModels() {
2124 ];2131 ];
2125}2132}
21262133
2134async function loadWorkersAIImageModels() {
2135 $('#sd_cf_workers_key').toggleClass('success', !!secret_state[SECRET_KEYS.WORKERS_AI]);
2136
2137 if (!secret_state[SECRET_KEYS.WORKERS_AI]) {
2138 return [];
2139 }
2140
2141 if (!oai_settings.workers_ai_account_id) {
2142 toastr.warning('Workers AI account ID is required. Save it in the "API Connections" panel.', 'Image Generation');
2143 return [];
2144 }
2145
2146 const result = await fetch('/api/sd/workersai/models', {
2147 method: 'POST',
2148 headers: getRequestHeaders(),
2149 body: JSON.stringify({
2150 account_id: oai_settings.workers_ai_account_id,
2151 }),
2152 });
2153
2154 if (result.ok) {
2155 return await result.json();
2156 }
2157
2158 return [];
2159}
2160
2127async function loadPollinationsModels() {2161async function loadPollinationsModels() {
2128 $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);2162 $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);
21292163
@@ -2609,6 +2643,9 @@ async function loadSchedulers() {
2609 case sources.openrouter:2643 case sources.openrouter:
2610 schedulers = ['N/A'];2644 schedulers = ['N/A'];
2611 break;2645 break;
2646 case sources.workersai:
2647 schedulers = ['N/A'];
2648 break;
2612 }2649 }
26132650
2614 for (const scheduler of schedulers) {2651 for (const scheduler of schedulers) {
@@ -2729,6 +2766,9 @@ async function loadVaes() {
2729 case sources.openrouter:2766 case sources.openrouter:
2730 vaes = ['N/A'];2767 vaes = ['N/A'];
2731 break;2768 break;
2769 case sources.workersai:
2770 vaes = ['N/A'];
2771 break;
2732 }2772 }
27332773
2734 for (const vae of vaes) {2774 for (const vae of vaes) {
@@ -3432,6 +3472,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
3432 case sources.openrouter:3472 case sources.openrouter:
3433 result = await generateOpenRouterImage(prefixedPrompt, signal);3473 result = await generateOpenRouterImage(prefixedPrompt, signal);
3434 break;3474 break;
3475 case sources.workersai:
3476 result = await generateWorkersAIImage(prefixedPrompt, negativePrompt, signal);
3477 break;
3435 }3478 }
34363479
3437 if (!result.data) {3480 if (!result.data) {
@@ -4748,6 +4791,33 @@ async function generateOpenRouterImage(prompt, signal) {
4748 throw new Error(text);4791 throw new Error(text);
4749}4792}
47504793
4794async function generateWorkersAIImage(prompt, negativePrompt, signal) {
4795 const result = await fetch('/api/sd/workersai/generate', {
4796 method: 'POST',
4797 headers: getRequestHeaders(),
4798 signal: signal,
4799 body: JSON.stringify({
4800 prompt: prompt,
4801 negative_prompt: negativePrompt,
4802 model: extension_settings.sd.model,
4803 width: extension_settings.sd.width,
4804 height: extension_settings.sd.height,
4805 steps: extension_settings.sd.steps,
4806 scale: extension_settings.sd.scale,
4807 seed: extension_settings.sd.seed >= 0 ? extension_settings.sd.seed : undefined,
4808 account_id: oai_settings.workers_ai_account_id,
4809 }),
4810 });
4811
4812 if (result.ok) {
4813 const data = await result.json();
4814 return { format: data?.format, data: data?.image };
4815 } else {
4816 const text = await result.text();
4817 throw new Error(text);
4818 }
4819}
4820
4751async function onComfyOpenWorkflowEditorClick() {4821async function onComfyOpenWorkflowEditorClick() {
4752 let workflow = await (await fetch('/api/sd/comfy/workflow', {4822 let workflow = await (await fetch('/api/sd/comfy/workflow', {
4753 method: 'POST',4823 method: 'POST',
@@ -5120,6 +5190,8 @@ function isValidState() {
5120 return secret_state[SECRET_KEYS.ZAI];5190 return secret_state[SECRET_KEYS.ZAI];
5121 case sources.openrouter:5191 case sources.openrouter:
5122 return secret_state[SECRET_KEYS.OPENROUTER];5192 return secret_state[SECRET_KEYS.OPENROUTER];
5193 case sources.workersai:
5194 return !!oai_settings.workers_ai_account_id && secret_state[SECRET_KEYS.WORKERS_AI];
5123 default:5195 default:
5124 return false;5196 return false;
5125 }5197 }
@@ -5879,6 +5951,9 @@ export async function init() {
5879 extension_settings.sd.google_duration = Number($(this).val());5951 extension_settings.sd.google_duration = Number($(this).val());
5880 saveSettingsDebounced();5952 saveSettingsDebounced();
5881 });5953 });
5954 $('#sd_models_refresh').on('click', async () => {
5955 await loadModels();
5956 });
5882 $('#sd_electronhub_quality').on('change', function () {5957 $('#sd_electronhub_quality').on('change', function () {
5883 extension_settings.sd.electronhub_quality = String($(this).val());5958 extension_settings.sd.electronhub_quality = String($(this).val());
5884 saveSettingsDebounced();5959 saveSettingsDebounced();
@@ -5922,6 +5997,7 @@ export async function init() {
5922 [sources.aimlapi]: SECRET_KEYS.AIMLAPI,5997 [sources.aimlapi]: SECRET_KEYS.AIMLAPI,
5923 [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD,5998 [sources.comfy]: SECRET_KEYS.COMFY_RUNPOD,
5924 [sources.pollinations]: SECRET_KEYS.POLLINATIONS,5999 [sources.pollinations]: SECRET_KEYS.POLLINATIONS,
6000 [sources.workersai]: SECRET_KEYS.WORKERS_AI,
5925 };6001 };
5926 const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);6002 const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);
5927 if (!shouldReloadOptions) {6003 if (!shouldReloadOptions) {
public/scripts/extensions/stable-diffusion/settings.html+36 -16
@@ -40,10 +40,11 @@
40 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>40 <span data-i18n="sd_minimal_prompt_processing_txt">Minimal response prompt processing</span>
41 </label>41 </label>
42 <label for="sd_source" data-i18n="Source">Source</label>42 <label for="sd_source" data-i18n="Source">Source</label>
43 <select id="sd_source">43 <select id="sd_source" class="text_pole">
44 <option value="aimlapi">AI/ML API</option>44 <option value="aimlapi">AI/ML API</option>
45 <option value="bfl">BFL (Black Forest Labs)</option>45 <option value="bfl">BFL (Black Forest Labs)</option>
46 <option value="chutes">Chutes</option>46 <option value="chutes">Chutes</option>
47 <option value="workersai">Cloudflare Workers AI</option>
47 <option value="comfy">ComfyUI</option>48 <option value="comfy">ComfyUI</option>
48 <option value="drawthings">DrawThings HTTP API</option>49 <option value="drawthings">DrawThings HTTP API</option>
49 <option value="electronhub">Electron Hub</option>50 <option value="electronhub">Electron Hub</option>
@@ -123,7 +124,7 @@
123 <div class="flex-container" id="sd_electronhub_quality_row">124 <div class="flex-container" id="sd_electronhub_quality_row">
124 <div class="flex1">125 <div class="flex1">
125 <label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label>126 <label for="sd_electronhub_quality" data-i18n="Image Quality">Image Quality</label>
126 <select id="sd_electronhub_quality"></select>127 <select id="sd_electronhub_quality" class="text_pole"></select>
127 </div>128 </div>
128 </div>129 </div>
129 </div>130 </div>
@@ -191,14 +192,14 @@
191 <div class="flex-container">192 <div class="flex-container">
192 <div data-sd-model="dall-e-3" class="flex1">193 <div data-sd-model="dall-e-3" class="flex1">
193 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>194 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
194 <select id="sd_openai_style">195 <select id="sd_openai_style" class="text_pole">
195 <option value="vivid">Vivid</option>196 <option value="vivid">Vivid</option>
196 <option value="natural">Natural</option>197 <option value="natural">Natural</option>
197 </select>198 </select>
198 </div>199 </div>
199 <div data-sd-model="gpt-image" class="flex1">200 <div data-sd-model="gpt-image" class="flex1">
200 <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label>201 <label for="sd_openai_quality_gpt" data-i18n="Image Quality">Image Quality</label>
201 <select id="sd_openai_quality_gpt">202 <select id="sd_openai_quality_gpt" class="text_pole">
202 <option value="auto" data-i18n="Auto">Auto</option>203 <option value="auto" data-i18n="Auto">Auto</option>
203 <option value="low" data-i18n="Low">Low</option>204 <option value="low" data-i18n="Low">Low</option>
204 <option value="medium" data-i18n="Medium">Medium</option>205 <option value="medium" data-i18n="Medium">Medium</option>
@@ -207,7 +208,7 @@
207 </div>208 </div>
208 <div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1">209 <div data-sd-model="dall-e-3,cogview-4,glm-image,cogvideox" class="flex1">
209 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>210 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
210 <select id="sd_openai_quality">211 <select id="sd_openai_quality" class="text_pole">
211 <option value="standard" data-i18n="Standard">Standard</option>212 <option value="standard" data-i18n="Standard">Standard</option>
212 <option value="hd" data-i18n="HD">HD</option>213 <option value="hd" data-i18n="HD">HD</option>
213 </select>214 </select>
@@ -216,7 +217,7 @@
216 <div data-sd-model="sora-2,sora-2-pro" class="flex-container">217 <div data-sd-model="sora-2,sora-2-pro" class="flex-container">
217 <div class="flex1">218 <div class="flex1">
218 <label for="sd_openai_duration" data-i18n="Duration">Duration</label>219 <label for="sd_openai_duration" data-i18n="Duration">Duration</label>
219 <select id="sd_openai_duration">220 <select id="sd_openai_duration" class="text_pole">
220 <option value="4" data-i18n="Short (4 seconds)">Short (4 seconds)</option>221 <option value="4" data-i18n="Short (4 seconds)">Short (4 seconds)</option>
221 <option value="8" data-i18n="Medium (8 seconds)">Medium (8 seconds)</option>222 <option value="8" data-i18n="Medium (8 seconds)">Medium (8 seconds)</option>
222 <option value="12" data-i18n="Long (16 seconds)">Long (12 seconds)</option>223 <option value="12" data-i18n="Long (16 seconds)">Long (12 seconds)</option>
@@ -226,7 +227,7 @@
226 </div>227 </div>
227 <div data-sd-source="comfy">228 <div data-sd-source="comfy">
228 <label for="sd_comfy_type">Server Type</label>229 <label for="sd_comfy_type">Server Type</label>
229 <select id="sd_comfy_type">230 <select id="sd_comfy_type" class="text_pole">
230 <option value="standard">Standard Server</option>231 <option value="standard">Standard Server</option>
231 <option value="runpod_serverless">RunPod Serverless Endpoint</option>232 <option value="runpod_serverless">RunPod Serverless Endpoint</option>
232 </select>233 </select>
@@ -318,7 +319,7 @@
318 <div class="flex-container">319 <div class="flex-container">
319 <div class="flex1">320 <div class="flex1">
320 <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label>321 <label for="sd_stability_style_preset" data-i18n="Style Preset">Style Preset</label>
321 <select id="sd_stability_style_preset">322 <select id="sd_stability_style_preset" class="text_pole">
322 <option value="anime">Anime</option>323 <option value="anime">Anime</option>
323 <option value="3d-model">3D Model</option>324 <option value="3d-model">3D Model</option>
324 <option value="analog-film">Analog Film</option>325 <option value="analog-film">Analog Film</option>
@@ -375,6 +376,20 @@
375 </div>376 </div>
376 </div>377 </div>
377378
379 <div data-sd-source="workersai">
380 <a href="https://dash.cloudflare.com" target="_blank" rel="noopener noreferrer">Cloudflare Workers AI</a>
381 <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
382 <strong class="flex1" data-i18n="API Key">API Key</strong>
383 <div id="sd_cf_workers_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_workers_ai">
384 <i class="fa-fw fa-solid fa-key"></i>
385 <span data-i18n="Click to set">Click to set</span>
386 </div>
387 </div>
388 <div class="flex-container flexnowrap alignItemsBaseline">
389 <small class="flex1" data-i18n="Hint: Account ID and API key are pulled from API connections.">Hint: Account ID and API key are pulled from API connections.</small>
390 </div>
391 </div>
392
378 <div data-sd-source="google">393 <div data-sd-source="google">
379 <div class="flex-container">394 <div class="flex-container">
380 <div class="flex1">395 <div class="flex1">
@@ -394,7 +409,7 @@
394 </label>409 </label>
395 <div class="flex1">410 <div class="flex1">
396 <label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label>411 <label for="sd_google_duration" data-i18n="Duration (Veo)">Duration (Veo)</label>
397 <select id="sd_google_duration">412 <select id="sd_google_duration" class="text_pole">
398 <option value="4">Short (4 seconds)</option>413 <option value="4">Short (4 seconds)</option>
399 <option value="6">Medium (6 seconds)</option>414 <option value="6">Medium (6 seconds)</option>
400 <option value="8">Long (8 seconds)</option>415 <option value="8">Long (8 seconds)</option>
@@ -405,37 +420,42 @@
405420
406 <div class="flex-container">421 <div class="flex-container">
407 <div class="flex1">422 <div class="flex1">
408 <label for="sd_model" data-i18n="Model">Model</label>423 <label for="sd_model" class="flex-container justifySpaceBetween">
409 <select id="sd_model"></select>424 <span data-i18n="Model">Model</span>
425 <div id="sd_models_refresh" class="right_menu_button margin0 padding0" title="Refresh model list" data-i18n="[title]Refresh model list">
426 <i class="fa-solid fa-sync"></i>
427 </div>
428 </label>
429 <select id="sd_model" class="text_pole"></select>
410 </div>430 </div>
411431
412 <div class="flex1" data-sd-source="comfy,auto">432 <div class="flex1" data-sd-source="comfy,auto">
413 <label for="sd_vae">VAE</label>433 <label for="sd_vae">VAE</label>
414 <select id="sd_vae"></select>434 <select id="sd_vae" class="text_pole"></select>
415 </div>435 </div>
416 </div>436 </div>
417437
418 <div class="flex-container">438 <div class="flex-container">
419 <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp">439 <div class="flex1" data-sd-source="extras,horde,auto,drawthings,novel,vlad,comfy,sdcpp">
420 <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>440 <label for="sd_sampler" data-i18n="Sampling method">Sampling method</label>
421 <select id="sd_sampler"></select>441 <select id="sd_sampler" class="text_pole"></select>
422 </div>442 </div>
423443
424 <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp">444 <div class="flex1" data-sd-source="comfy,auto,novel,sdcpp">
425 <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>445 <label for="sd_scheduler" data-i18n="Scheduler">Scheduler</label>
426 <select id="sd_scheduler"></select>446 <select id="sd_scheduler" class="text_pole"></select>
427 </div>447 </div>
428 </div>448 </div>
429449
430 <div class="flex-container">450 <div class="flex-container">
431 <div class="flex1">451 <div class="flex1">
432 <label for="sd_resolution" data-i18n="Resolution">Resolution</label>452 <label for="sd_resolution" data-i18n="Resolution">Resolution</label>
433 <select id="sd_resolution"><!-- Populated in JS --></select>453 <select id="sd_resolution" class="text_pole"><!-- Populated in JS --></select>
434 </div>454 </div>
435455
436 <div class="flex1" data-sd-source="auto,vlad,drawthings">456 <div class="flex1" data-sd-source="auto,vlad,drawthings">
437 <label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>457 <label for="sd_hr_upscaler" data-i18n="Upscaler">Upscaler</label>
438 <select id="sd_hr_upscaler"></select>458 <select id="sd_hr_upscaler" class="text_pole"></select>
439 </div>459 </div>
440 </div>460 </div>
441461
public/scripts/extensions/stable-diffusion/style.css+0 -4
@@ -1,7 +1,3 @@
1.sd_settings label:not(.checkbox_label) {
2 display: block;
3}
4
5#sd_dropdown {1#sd_dropdown {
6 z-index: 30000;2 z-index: 30000;
7 backdrop-filter: blur(var(--SmartThemeBlurStrength));3 backdrop-filter: blur(var(--SmartThemeBlurStrength));
src/endpoints/stable-diffusion.js+140 -1
@@ -5,7 +5,6 @@ import express from 'express';
5import fetch from 'node-fetch';5import fetch from 'node-fetch';
6import sanitize from 'sanitize-filename';6import sanitize from 'sanitize-filename';
7import { sync as writeFileAtomicSync } from 'write-file-atomic';7import { sync as writeFileAtomicSync } from 'write-file-atomic';
8import FormData from 'form-data';
9import urlJoin from 'url-join';8import urlJoin from 'url-join';
10import _ from 'lodash';9import _ from 'lodash';
11import mime from 'mime-types';10import mime from 'mime-types';
@@ -2031,6 +2030,145 @@ zai.post('/generate-video', async (request, response) => {
2031 }2030 }
2032});2031});
20332032
2033const workersai = express.Router();
2034
2035workersai.post('/models', async (request, response) => {
2036 try {
2037 const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
2038
2039 if (!key) {
2040 console.warn('Cloudflare Workers AI API key not found.');
2041 return response.sendStatus(400);
2042 }
2043
2044 const accountId = String(request.body.account_id || '').trim();
2045 if (!accountId) {
2046 console.warn('Cloudflare Workers AI Account ID not found.');
2047 return response.sendStatus(400);
2048 }
2049
2050 const apiUrl = new URL(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/models/search`);
2051 apiUrl.searchParams.set('task', 'Text-to-Image');
2052 apiUrl.searchParams.set('per_page', '1000');
2053 const result = await fetch(apiUrl, {
2054 method: 'GET',
2055 headers: {
2056 'Authorization': `Bearer ${key}`,
2057 },
2058 });
2059
2060 if (!result.ok) {
2061 console.warn('Cloudflare Workers AI returned an error.', result.statusText);
2062 return response.sendStatus(500);
2063 }
2064
2065 /** @type {any} */
2066 const data = await result.json();
2067
2068 if (!data.success || !Array.isArray(data.result)) {
2069 console.warn('Cloudflare Workers AI returned invalid data.');
2070 return response.sendStatus(500);
2071 }
2072
2073 const models = data.result.map(x => ({ value: x.name, text: x.name }));
2074 return response.send(models);
2075 } catch (error) {
2076 console.error(error);
2077 return response.sendStatus(500);
2078 }
2079});
2080
2081workersai.post('/generate', async (request, response) => {
2082 try {
2083 const key = readSecret(request.user.directories, SECRET_KEYS.WORKERS_AI);
2084
2085 if (!key) {
2086 console.warn('Cloudflare Workers AI API key not found.');
2087 return response.sendStatus(400);
2088 }
2089
2090 const accountId = String(request.body.account_id || '').trim();
2091 if (!accountId) {
2092 console.warn('Cloudflare Workers AI Account ID not found.');
2093 return response.sendStatus(400);
2094 }
2095
2096 const model = String(request.body.model || '').trim();
2097 if (!model) {
2098 console.warn('Cloudflare Workers AI model not specified.');
2099 return response.sendStatus(400);
2100 }
2101
2102 const apiUrl = `https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(accountId)}/ai/run/${model}`;
2103
2104 const body = {
2105 prompt: request.body.prompt,
2106 negative_prompt: request.body.negative_prompt || undefined,
2107 width: request.body.width ? Number(request.body.width) : undefined,
2108 height: request.body.height ? Number(request.body.height) : undefined,
2109 num_steps: request.body.steps ? Number(request.body.steps) : undefined,
2110 guidance: request.body.scale ? Number(request.body.scale) : undefined,
2111 seed: request.body.seed >= 0 ? Number(request.body.seed) : undefined,
2112 };
2113
2114 // Remove undefined values
2115 for (const prop of Object.keys(body)) {
2116 if (body[prop] === undefined) {
2117 delete body[prop];
2118 }
2119 }
2120
2121 console.debug('Cloudflare Workers AI request:', model, body);
2122
2123 /** @type {import('node-fetch').RequestInit} */
2124 const apiRequest = {
2125 method: 'POST',
2126 headers: {
2127 'Authorization': `Bearer ${key}`,
2128 },
2129 };
2130
2131 if (/flux-2/.test(model)) {
2132 const formData = new FormData();
2133 for (const [key, value] of Object.entries(body)) {
2134 formData.append(key, String(value));
2135 }
2136 apiRequest.body = formData;
2137 } else {
2138 apiRequest.headers = { ...apiRequest.headers, 'Content-Type': 'application/json' };
2139 apiRequest.body = JSON.stringify(body);
2140 }
2141
2142 const result = await fetch(apiUrl, apiRequest);
2143 if (!result.ok) {
2144 const text = await result.text();
2145 console.warn('Cloudflare Workers AI returned an error.', result.status, result.statusText, text);
2146 return response.status(500).send(text);
2147 }
2148
2149 const contentType = result.headers.get('content-type') || '';
2150
2151 // Partner models return JSON with base64 image
2152 if (contentType.includes('application/json')) {
2153 /** @type {any} */
2154 const data = await result.json();
2155 const image = data?.result?.image || data?.image;
2156 if (!image) {
2157 console.warn('Cloudflare Workers AI returned JSON without image data.');
2158 return response.sendStatus(500);
2159 }
2160 return response.send({ format: 'png', image: image });
2161 }
2162
2163 // Non-partner models return raw binary image data
2164 const buffer = await result.arrayBuffer();
2165 return response.send({ format: 'png', image: Buffer.from(buffer).toString('base64') });
2166 } catch (error) {
2167 console.error(error);
2168 return response.sendStatus(500);
2169 }
2170});
2171
2034router.use('/comfy', comfy);2172router.use('/comfy', comfy);
2035router.use('/comfyrunpod', comfyRunPod);2173router.use('/comfyrunpod', comfyRunPod);
2036router.use('/together', together);2174router.use('/together', together);
@@ -2047,3 +2185,4 @@ router.use('/falai', falai);
2047router.use('/xai', xai);2185router.use('/xai', xai);
2048router.use('/aimlapi', aimlapi);2186router.use('/aimlapi', aimlapi);
2049router.use('/zai', zai);2187router.use('/zai', zai);
2188router.use('/workersai', workersai);