Z.AI: Add image generation and web search (#4895) * Z.AI: Add image generation * Z.AI: Add web search endpoint * Add image URL validation * Combine validation conditions

bb3ea08b5a3872d9cb4fcf5dab9fef4b80fcd8be

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
5 files changed, +187 -6Showing whitespace changes
public/scripts/extensions/stable-diffusion/index.js+70 -1
@@ -90,6 +90,7 @@ const sources = {
9090 falai: 'falai',
9191 xai: 'xai',
9292 google: 'google',
93+ zai: 'zai',
9394};
9495
9596const initiators = {
@@ -1323,6 +1324,7 @@ async function onModelChange() {
13231324 sources.xai,
13241325 sources.google,
13251326 sources.chutes,
1327+ sources.zai,
13261328 ];
13271329
13281330 if (cloudSources.includes(extension_settings.sd.source)) {
@@ -1547,12 +1549,18 @@ async function loadSamplers() {
15471549 case sources.bfl:
15481550 samplers = ['N/A'];
15491551 break;
1552+ case sources.falai:
1553+ samplers = ['N/A'];
1554+ break;
15501555 case sources.xai:
15511556 samplers = ['N/A'];
15521557 break;
15531558 case sources.google:
15541559 samplers = ['N/A'];
15551560 break;
1561+ case sources.zai:
1562+ samplers = ['N/A'];
1563+ break;
15561564 }
15571565
15581566 for (const sampler of samplers) {
@@ -1758,6 +1766,9 @@ async function loadModels() {
17581766 case sources.google:
17591767 models = await loadGoogleModels();
17601768 break;
1769+ case sources.zai:
1770+ models = await loadZaiModels();
1771+ break;
17611772 }
17621773
17631774 if (extension_settings.sd.source === sources.electronhub) {
@@ -2236,6 +2247,10 @@ async function loadGoogleModels() {
22362247 ].map(name => ({ value: name, text: name }));
22372248}
22382249
2250+async function loadZaiModels() {
2251+ return ['cogview-4-250304'].map(name => ({ value: name, text: name }));
2252+}
2253+
22392254function loadNovelSchedulers() {
22402255 return ['karras', 'native', 'exponential', 'polyexponential'];
22412256}
@@ -2327,6 +2342,9 @@ async function loadSchedulers() {
23272342 case sources.google:
23282343 schedulers = ['N/A'];
23292344 break;
2345+ case sources.zai:
2346+ schedulers = ['N/A'];
2347+ break;
23302348 }
23312349
23322350 for (const scheduler of schedulers) {
@@ -2430,6 +2448,9 @@ async function loadVaes() {
24302448 case sources.google:
24312449 vaes = ['N/A'];
24322450 break;
2451+ case sources.zai:
2452+ vaes = ['N/A'];
2453+ break;
24332454 }
24342455
24352456 for (const vae of vaes) {
@@ -2569,7 +2590,7 @@ function processReply(str) {
25692590 str = str.normalize('NFD');
25702591
25712592 // Strip out non-alphanumeric characters barring model syntax exceptions
25722593 str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' ');
25732594
25742595 str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one
25752596 str = str.trim();
@@ -3025,6 +3046,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP
30253046 case sources.google:
30263047 result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal);
30273048 break;
3049+ case sources.zai:
3050+ result = await generateZaiImage(prefixedPrompt, signal);
3051+ break;
30283052 }
30293053
30303054 if (!result.data) {
@@ -4094,6 +4118,47 @@ async function generateGoogleImage(prompt, negativePrompt, signal) {
40944118 }
40954119}
40964120
4121+/**
4122+ * Generates an image using the Z.AI API.
4123+ * @param {string} prompt The main instruction used to guide the image generation.
4124+ * @param {AbortSignal} signal An AbortSignal object that can be used to cancel the request.
4125+ * @returns {Promise<{format: string, data: string}>} A promise that resolves when the image generation and processing are complete.
4126+ */
4127+async function generateZaiImage(prompt, signal) {
4128+ // Round width and height to nearest multiple of 16, and clamp to 512-2048 range
4129+ let width = clamp(Math.round(extension_settings.sd.width / 16) * 16, 512, 2048);
4130+ let height = clamp(Math.round(extension_settings.sd.height / 16) * 16, 512, 2048);
4131+
4132+ // Make sure the pixel count does not exceed 2^21px
4133+ while ((width * height) > Math.pow(2, 21)) {
4134+ if (width >= height) {
4135+ width -= 16;
4136+ } else {
4137+ height -= 16;
4138+ }
4139+ }
4140+
4141+ const result = await fetch('/api/sd/zai/generate', {
4142+ method: 'POST',
4143+ headers: getRequestHeaders(),
4144+ signal: signal,
4145+ body: JSON.stringify({
4146+ prompt: prompt,
4147+ model: extension_settings.sd.model,
4148+ quality: extension_settings.sd.openai_quality,
4149+ size: `${width}x${height}`,
4150+ }),
4151+ });
4152+
4153+ if (result.ok) {
4154+ const data = await result.json();
4155+ return { format: data.format, data: data.image };
4156+ }
4157+
4158+ const text = await result.text();
4159+ throw new Error(text);
4160+}
4161+
40974162async function onComfyOpenWorkflowEditorClick() {
40984163 let workflow = await (await fetch('/api/sd/comfy/workflow', {
40994164 method: 'POST',
@@ -4400,6 +4465,10 @@ function isValidState() {
44004465 return secret_state[SECRET_KEYS.XAI];
44014466 case sources.google:
44024467 return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT];
4468+ case sources.zai:
4469+ return secret_state[SECRET_KEYS.ZAI];
4470+ default:
4471+ return false;
44034472 }
44044473}
44054474
public/scripts/extensions/stable-diffusion/settings.html+8 -4
@@ -57,6 +57,7 @@
5757 <option value="horde">Stable Horde</option>
5858 <option value="togetherai">TogetherAI</option>
5959 <option value="xai">xAI (Grok)</option>
60+ <option value="zai">Z.AI (CogView)</option>
6061 </select>
6162 <div data-sd-source="auto">
6263 <label for="sd_auto_url">SD Web UI URL</label>
@@ -164,16 +165,19 @@
164165 </div>
165166 </div>
166167 </div>
167168 <div data-sd-source="openai,aimlapizai">
168- <div data-sd-model="dall-e-3" class="flex-container">
169+ <b>Will use Common API. Coding API is not supported!</b>
169- <div class="flex1">
170+ </div>
171+ <div data-sd-source="openai,aimlapi,zai">
172+ <div class="flex-container">
173+ <div data-sd-model="dall-e-3" class="flex1">
170174 <label for="sd_openai_style" data-i18n="Image Style">Image Style</label>
171175 <select id="sd_openai_style">
172176 <option value="vivid">Vivid</option>
173177 <option value="natural">Natural</option>
174178 </select>
175179 </div>
176180 <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1">
177181 <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label>
178182 <select id="sd_openai_quality">
179183 <option value="standard" data-i18n="Standard">Standard</option>
src/endpoints/openai.js+1 -0
@@ -91,6 +91,7 @@ router.post('/caption-image', async (request, response) => {
9191
9292 if (request.body.api === 'zai') {
9393 key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
94+ bodyParams.max_tokens = 4096; // default is 1024
9495 }
9596
9697 const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];
src/endpoints/search.js+46 -0
@@ -341,6 +341,52 @@ router.post('/serper', async (request, response) => {
341341 }
342342});
343343
344+router.post('/zai', async (request, response) => {
345+ try {
346+ const key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
347+
348+ if (!key) {
349+ console.error('No Z.AI key found');
350+ return response.sendStatus(400);
351+ }
352+
353+ const { query } = request.body;
354+
355+ if (!query) {
356+ console.error('No query provided for /zai');
357+ return response.sendStatus(400);
358+ }
359+
360+ console.debug('Z.AI web search query', query);
361+
362+ const result = await fetch('https://api.z.ai/api/paas/v4/web_search', {
363+ method: 'POST',
364+ headers: {
365+ 'Content-Type': 'application/json',
366+ 'Authorization': `Bearer ${key}`,
367+ },
368+ body: JSON.stringify({
369+ // TODO: There's only one engine option for now
370+ search_engine: 'search-prime',
371+ search_query: query,
372+ }),
373+ });
374+
375+ if (!result.ok) {
376+ const text = await result.text();
377+ console.error('Z.AI request failed', result.statusText, text);
378+ return response.status(500).send(text);
379+ }
380+
381+ const data = await result.json();
382+ console.debug('Z.AI web search response', data);
383+ return response.json(data);
384+ } catch (error) {
385+ console.error(error);
386+ return response.sendStatus(500);
387+ }
388+});
389+
344390router.post('/visit', async (request, response) => {
345391 try {
346392 const url = request.body.url;
src/endpoints/stable-diffusion.js+62 -1
@@ -9,7 +9,7 @@ import FormData from 'form-data';
99import urlJoin from 'url-join';
1010import _ from 'lodash';
1111
1212import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';
1313import { readSecret, SECRET_KEYS } from './secrets.js';
1414import { AIMLAPI_HEADERS } from '../constants.js';
1515
@@ -1643,6 +1643,66 @@ aimlapi.post('/generate-image', async (req, res) => {
16431643 }
16441644});
16451645
1646+const zai = express.Router();
1647+
1648+zai.post('/generate', async (request, response) => {
1649+ try {
1650+ const key = readSecret(request.user.directories, SECRET_KEYS.ZAI);
1651+
1652+ if (!key) {
1653+ console.warn('Z.AI key not found.');
1654+ return response.sendStatus(400);
1655+ }
1656+
1657+ console.debug('Z.AI image request:', request.body);
1658+
1659+ const generateResponse = await fetch('https://api.z.ai/api/paas/v4/images/generations', {
1660+ method: 'POST',
1661+ headers: {
1662+ 'Content-Type': 'application/json',
1663+ 'Authorization': `Bearer ${key}`,
1664+ },
1665+ body: JSON.stringify({
1666+ prompt: request.body.prompt,
1667+ model: request.body.model,
1668+ quality: request.body.quality,
1669+ size: request.body.size,
1670+ }),
1671+ });
1672+
1673+ if (!generateResponse.ok) {
1674+ const text = await generateResponse.text();
1675+ console.warn('Z.AI returned an error.', text);
1676+ return response.sendStatus(500);
1677+ }
1678+
1679+ /** @type {any} */
1680+ const data = await generateResponse.json();
1681+ console.debug('Z.AI image response:', data);
1682+
1683+ const url = data?.data?.[0]?.url;
1684+ if (!url || !isValidUrl(url) || !new URL(url).hostname.endsWith('.z.ai')) {
1685+ console.warn('Z.AI returned an invalid image URL.');
1686+ return response.sendStatus(500);
1687+ }
1688+
1689+ const imageResponse = await fetch(url);
1690+ if (!imageResponse.ok) {
1691+ console.warn('Z.AI image fetch returned an error.');
1692+ return response.sendStatus(500);
1693+ }
1694+
1695+ const buffer = await imageResponse.arrayBuffer();
1696+ const image = Buffer.from(buffer).toString('base64');
1697+ const format = path.extname(url).substring(1).toLowerCase() || 'png';
1698+
1699+ return response.send({ image, format });
1700+ } catch (error) {
1701+ console.error(error);
1702+ return response.sendStatus(500);
1703+ }
1704+});
1705+
16461706router.use('/comfy', comfy);
16471707router.use('/together', together);
16481708router.use('/drawthings', drawthings);
@@ -1656,3 +1716,4 @@ router.use('/bfl', bfl);
16561716router.use('/falai', falai);
16571717router.use('/xai', xai);
16581718router.use('/aimlapi', aimlapi);
1719+router.use('/zai', zai);