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
Signed| @@ -90,6 +90,7 @@ const sources = { | |||
| 90 | falai: 'falai', | 90 | falai: 'falai', |
| 91 | xai: 'xai', | 91 | xai: 'xai', |
| 92 | google: 'google', | 92 | google: 'google', |
| 93 | zai: 'zai', | ||
| 93 | }; | 94 | }; |
| 94 | 95 | ||
| 95 | const initiators = { | 96 | const initiators = { |
| @@ -1323,6 +1324,7 @@ async function onModelChange() { | |||
| 1323 | sources.xai, | 1324 | sources.xai, |
| 1324 | sources.google, | 1325 | sources.google, |
| 1325 | sources.chutes, | 1326 | sources.chutes, |
| 1327 | sources.zai, | ||
| 1326 | ]; | 1328 | ]; |
| 1327 | 1329 | ||
| 1328 | if (cloudSources.includes(extension_settings.sd.source)) { | 1330 | if (cloudSources.includes(extension_settings.sd.source)) { |
| @@ -1547,12 +1549,18 @@ async function loadSamplers() { | |||
| 1547 | case sources.bfl: | 1549 | case sources.bfl: |
| 1548 | samplers = ['N/A']; | 1550 | samplers = ['N/A']; |
| 1549 | break; | 1551 | break; |
| 1552 | case sources.falai: | ||
| 1553 | samplers = ['N/A']; | ||
| 1554 | break; | ||
| 1550 | case sources.xai: | 1555 | case sources.xai: |
| 1551 | samplers = ['N/A']; | 1556 | samplers = ['N/A']; |
| 1552 | break; | 1557 | break; |
| 1553 | case sources.google: | 1558 | case sources.google: |
| 1554 | samplers = ['N/A']; | 1559 | samplers = ['N/A']; |
| 1555 | break; | 1560 | break; |
| 1561 | case sources.zai: | ||
| 1562 | samplers = ['N/A']; | ||
| 1563 | break; | ||
| 1556 | } | 1564 | } |
| 1557 | 1565 | ||
| 1558 | for (const sampler of samplers) { | 1566 | for (const sampler of samplers) { |
| @@ -1758,6 +1766,9 @@ async function loadModels() { | |||
| 1758 | case sources.google: | 1766 | case sources.google: |
| 1759 | models = await loadGoogleModels(); | 1767 | models = await loadGoogleModels(); |
| 1760 | break; | 1768 | break; |
| 1769 | case sources.zai: | ||
| 1770 | models = await loadZaiModels(); | ||
| 1771 | break; | ||
| 1761 | } | 1772 | } |
| 1762 | 1773 | ||
| 1763 | if (extension_settings.sd.source === sources.electronhub) { | 1774 | if (extension_settings.sd.source === sources.electronhub) { |
| @@ -2236,6 +2247,10 @@ async function loadGoogleModels() { | |||
| 2236 | ].map(name => ({ value: name, text: name })); | 2247 | ].map(name => ({ value: name, text: name })); |
| 2237 | } | 2248 | } |
| 2238 | 2249 | ||
| 2250 | async function loadZaiModels() { | ||
| 2251 | return ['cogview-4-250304'].map(name => ({ value: name, text: name })); | ||
| 2252 | } | ||
| 2253 | |||
| 2239 | function loadNovelSchedulers() { | 2254 | function loadNovelSchedulers() { |
| 2240 | return ['karras', 'native', 'exponential', 'polyexponential']; | 2255 | return ['karras', 'native', 'exponential', 'polyexponential']; |
| 2241 | } | 2256 | } |
| @@ -2327,6 +2342,9 @@ async function loadSchedulers() { | |||
| 2327 | case sources.google: | 2342 | case sources.google: |
| 2328 | schedulers = ['N/A']; | 2343 | schedulers = ['N/A']; |
| 2329 | break; | 2344 | break; |
| 2345 | case sources.zai: | ||
| 2346 | schedulers = ['N/A']; | ||
| 2347 | break; | ||
| 2330 | } | 2348 | } |
| 2331 | 2349 | ||
| 2332 | for (const scheduler of schedulers) { | 2350 | for (const scheduler of schedulers) { |
| @@ -2430,6 +2448,9 @@ async function loadVaes() { | |||
| 2430 | case sources.google: | 2448 | case sources.google: |
| 2431 | vaes = ['N/A']; | 2449 | vaes = ['N/A']; |
| 2432 | break; | 2450 | break; |
| 2451 | case sources.zai: | ||
| 2452 | vaes = ['N/A']; | ||
| 2453 | break; | ||
| 2433 | } | 2454 | } |
| 2434 | 2455 | ||
| 2435 | for (const vae of vaes) { | 2456 | for (const vae of vaes) { |
| @@ -2569,7 +2590,7 @@ function processReply(str) { | |||
| 2569 | str = str.normalize('NFD'); | 2590 | str = str.normalize('NFD'); |
| 2570 | 2591 | ||
| 2571 | // Strip out non-alphanumeric characters barring model syntax exceptions | 2592 | // Strip out non-alphanumeric characters barring model syntax exceptions |
| 2572 | str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]\-'|#]+/g, ' '); | 2593 | str = str.replace(/[^a-zA-Z0-9.,:_(){}<>[\]/\-'|#]+/g, ' '); |
| 2573 | 2594 | ||
| 2574 | str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one | 2595 | str = str.replace(/\s+/g, ' '); // Collapse multiple whitespaces into one |
| 2575 | str = str.trim(); | 2596 | str = str.trim(); |
| @@ -3025,6 +3046,9 @@ async function sendGenerationRequest(generationType, prompt, additionalNegativeP | |||
| 3025 | case sources.google: | 3046 | case sources.google: |
| 3026 | result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal); | 3047 | result = await generateGoogleImage(prefixedPrompt, negativePrompt, signal); |
| 3027 | break; | 3048 | break; |
| 3049 | case sources.zai: | ||
| 3050 | result = await generateZaiImage(prefixedPrompt, signal); | ||
| 3051 | break; | ||
| 3028 | } | 3052 | } |
| 3029 | 3053 | ||
| 3030 | if (!result.data) { | 3054 | if (!result.data) { |
| @@ -4094,6 +4118,47 @@ async function generateGoogleImage(prompt, negativePrompt, signal) { | |||
| 4094 | } | 4118 | } |
| 4095 | } | 4119 | } |
| 4096 | 4120 | ||
| 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 | |||
| 4097 | async function onComfyOpenWorkflowEditorClick() { | 4162 | async function onComfyOpenWorkflowEditorClick() { |
| 4098 | let workflow = await (await fetch('/api/sd/comfy/workflow', { | 4163 | let workflow = await (await fetch('/api/sd/comfy/workflow', { |
| 4099 | method: 'POST', | 4164 | method: 'POST', |
| @@ -4400,6 +4465,10 @@ function isValidState() { | |||
| 4400 | return secret_state[SECRET_KEYS.XAI]; | 4465 | return secret_state[SECRET_KEYS.XAI]; |
| 4401 | case sources.google: | 4466 | case sources.google: |
| 4402 | return secret_state[SECRET_KEYS.MAKERSUITE] || secret_state[SECRET_KEYS.VERTEXAI] || secret_state[SECRET_KEYS.VERTEXAI_SERVICE_ACCOUNT]; | 4467 | 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; | ||
| 4403 | } | 4472 | } |
| 4404 | } | 4473 | } |
| 4405 | 4474 | ||
| @@ -57,6 +57,7 @@ | |||
| 57 | <option value="horde">Stable Horde</option> | 57 | <option value="horde">Stable Horde</option> |
| 58 | <option value="togetherai">TogetherAI</option> | 58 | <option value="togetherai">TogetherAI</option> |
| 59 | <option value="xai">xAI (Grok)</option> | 59 | <option value="xai">xAI (Grok)</option> |
| 60 | <option value="zai">Z.AI (CogView)</option> | ||
| 60 | </select> | 61 | </select> |
| 61 | <div data-sd-source="auto"> | 62 | <div data-sd-source="auto"> |
| 62 | <label for="sd_auto_url">SD Web UI URL</label> | 63 | <label for="sd_auto_url">SD Web UI URL</label> |
| @@ -164,16 +165,19 @@ | |||
| 164 | </div> | 165 | </div> |
| 165 | </div> | 166 | </div> |
| 166 | </div> | 167 | </div> |
| 167 | <div data-sd-source="openai,aimlapi"> | 168 | <div data-sd-source="zai"> |
| 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"> | ||
| 170 | <label for="sd_openai_style" data-i18n="Image Style">Image Style</label> | 174 | <label for="sd_openai_style" data-i18n="Image Style">Image Style</label> |
| 171 | <select id="sd_openai_style"> | 175 | <select id="sd_openai_style"> |
| 172 | <option value="vivid">Vivid</option> | 176 | <option value="vivid">Vivid</option> |
| 173 | <option value="natural">Natural</option> | 177 | <option value="natural">Natural</option> |
| 174 | </select> | 178 | </select> |
| 175 | </div> | 179 | </div> |
| 176 | <div class="flex1"> | 180 | <div data-sd-model="dall-e-3,cogview-4-250304" class="flex1"> |
| 177 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> | 181 | <label for="sd_openai_quality" data-i18n="Image Quality">Image Quality</label> |
| 178 | <select id="sd_openai_quality"> | 182 | <select id="sd_openai_quality"> |
| 179 | <option value="standard" data-i18n="Standard">Standard</option> | 183 | <option value="standard" data-i18n="Standard">Standard</option> |
| @@ -91,6 +91,7 @@ router.post('/caption-image', async (request, response) => { | |||
| 91 | 91 | ||
| 92 | if (request.body.api === 'zai') { | 92 | if (request.body.api === 'zai') { |
| 93 | key = readSecret(request.user.directories, SECRET_KEYS.ZAI); | 93 | key = readSecret(request.user.directories, SECRET_KEYS.ZAI); |
| 94 | bodyParams.max_tokens = 4096; // default is 1024 | ||
| 94 | } | 95 | } |
| 95 | 96 | ||
| 96 | const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations']; | 97 | const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations']; |
| @@ -341,6 +341,52 @@ router.post('/serper', async (request, response) => { | |||
| 341 | } | 341 | } |
| 342 | }); | 342 | }); |
| 343 | 343 | ||
| 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 | |||
| 344 | router.post('/visit', async (request, response) => { | 390 | router.post('/visit', async (request, response) => { |
| 345 | try { | 391 | try { |
| 346 | const url = request.body.url; | 392 | const url = request.body.url; |
| @@ -9,7 +9,7 @@ import FormData from 'form-data'; | |||
| 9 | import urlJoin from 'url-join'; | 9 | import urlJoin from 'url-join'; |
| 10 | import _ from 'lodash'; | 10 | import _ from 'lodash'; |
| 11 | 11 | ||
| 12 | import { delay, getBasicAuthHeader, tryParse } from '../util.js'; | 12 | import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js'; |
| 13 | import { readSecret, SECRET_KEYS } from './secrets.js'; | 13 | import { readSecret, SECRET_KEYS } from './secrets.js'; |
| 14 | import { AIMLAPI_HEADERS } from '../constants.js'; | 14 | import { AIMLAPI_HEADERS } from '../constants.js'; |
| 15 | 15 | ||
| @@ -1643,6 +1643,66 @@ aimlapi.post('/generate-image', async (req, res) => { | |||
| 1643 | } | 1643 | } |
| 1644 | }); | 1644 | }); |
| 1645 | 1645 | ||
| 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 | |||
| 1646 | router.use('/comfy', comfy); | 1706 | router.use('/comfy', comfy); |
| 1647 | router.use('/together', together); | 1707 | router.use('/together', together); |
| 1648 | router.use('/drawthings', drawthings); | 1708 | router.use('/drawthings', drawthings); |
| @@ -1656,3 +1716,4 @@ router.use('/bfl', bfl); | |||
| 1656 | router.use('/falai', falai); | 1716 | router.use('/falai', falai); |
| 1657 | router.use('/xai', xai); | 1717 | router.use('/xai', xai); |
| 1658 | router.use('/aimlapi', aimlapi); | 1718 | router.use('/aimlapi', aimlapi); |
| 1719 | router.use('/zai', zai); | ||