Update Pollinations API (#5060) * Upgrade Pollinations API Done: text, caption To do: TTS, image Fixes #5020 * Update Pollinations TTS to new API * Update Pollinations API for images

5a7875ba282a0dc52378b997baa5730283a15b51

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

Signed
13 files changed, +129 -63Showing whitespace changes
public/index.html+12 -9
@@ -3772,19 +3772,22 @@
37723772 </select>
37733773 </div>
37743774 <div id="pollinations_form" data-source="pollinations">
3775+ <h4>
3776+ <a href="https://enter.pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Pollinations API Key">
3777+ Pollinations API Key
3778+ </a>
3779+ </h4>
3780+ <div class="flex-container">
3781+ <input id="api_key_pollinations" name="api_key_pollinations" class="text_pole flex1" value="" type="text" autocomplete="off">
3782+ <div title="Manage API keys" data-i18n="[title]Manage API keys" class="menu_button fa-solid fa-key fa-fw manage-api-keys" data-key="api_key_pollinations"></div>
3783+ </div>
3784+ <div data-for="api_key_pollinations" class="neutral_warning" data-i18n="For privacy reasons, your API key will be hidden after you click 'Connect'.">
3785+ For privacy reasons, your API key will be hidden after you click 'Connect'.
3786+ </div>
37753787 <h4 data-i18n="Pollinations Model">Pollinations Model</h4>
37763788 <select id="model_pollinations_select">
37773789 <!-- Populated by JavaScript -->
37783790 </select>
3779- <div class="info-block hint">
3780- <a href="https://pollinations.ai/" target="_blank" rel="noopener noreferrer" data-i18n="Provided free of charge by Pollinations.AI">
3781- Provided free of charge by Pollinations.AI
3782- </a>
3783- <br>
3784- <span data-i18n="Avoid sending sensitive information. Provider's outputs may include ads.">
3785- Avoid sending sensitive information. Provider's outputs may include ads.
3786- </span>
3787- </div>
37883791 </div>
37893792 <div id="moonshot_form" data-source="moonshot">
37903793 <h4>
public/scripts/RossAscends-mods.js+1 -1
@@ -408,7 +408,7 @@ function RA_autoconnect(PrevApi) {
408408 || (secret_state[SECRET_KEYS.FIREWORKS] && oai_settings.chat_completion_source == chat_completion_sources.FIREWORKS)
409409 || (secret_state[SECRET_KEYS.COMETAPI] && oai_settings.chat_completion_source == chat_completion_sources.COMETAPI)
410410 || (secret_state[SECRET_KEYS.ZAI] && oai_settings.chat_completion_source == chat_completion_sources.ZAI)
411411 || (secret_state[SECRET_KEYS.POLLINATIONS] && oai_settings.chat_completion_source === chat_completion_sources.POLLINATIONS)
412412 || (isValidUrl(oai_settings.custom_url) && oai_settings.chat_completion_source == chat_completion_sources.CUSTOM)
413413 || (secret_state[SECRET_KEYS.AZURE_OPENAI] && oai_settings.chat_completion_source == chat_completion_sources.AZURE_OPENAI)
414414 ) {
public/scripts/extensions/caption/index.js+2 -1
@@ -507,6 +507,7 @@ jQuery(async function () {
507507 'nanogpt': SECRET_KEYS.NANOGPT,
508508 'chutes': SECRET_KEYS.CHUTES,
509509 'electronhub': SECRET_KEYS.ELECTRONHUB,
510+ 'pollinations': SECRET_KEYS.POLLINATIONS,
510511 };
511512
512513 if (chatCompletionApis[api] && secret_state[chatCompletionApis[api]]) {
@@ -530,7 +531,7 @@ jQuery(async function () {
530531 }
531532
532533 // Custom API doesn't need additional checks
533534 if (api === 'custom' || api === 'pollinations') {
534535 return true;
535536 }
536537 }
public/scripts/extensions/shared.js+4 -0
@@ -279,6 +279,10 @@ function throwIfInvalidModel(useReverseProxy) {
279279 if (multimodalApi === 'zai' && !secret_state[SECRET_KEYS.ZAI]) {
280280 throw new Error('Z.AI API key is not set.');
281281 }
282+
283+ if (multimodalApi === 'pollinations' && !secret_state[SECRET_KEYS.POLLINATIONS]) {
284+ throw new Error('Pollinations API key is not set.');
285+ }
282286}
283287
284288/**
public/scripts/extensions/stable-diffusion/index.js+16 -10
@@ -1955,6 +1955,8 @@ async function loadXAIModels() {
19551955}
19561956
19571957async function loadPollinationsModels() {
1958+ $('#sd_pollinations_key').toggleClass('success', !!secret_state[SECRET_KEYS.POLLINATIONS]);
1959+
19581960 const result = await fetch('/api/sd/pollinations/models', {
19591961 method: 'POST',
19601962 headers: getRequestHeaders({ omitContentType: true }),
@@ -3312,7 +3314,7 @@ async function generatePollinationsImage(prompt, negativePrompt, signal) {
33123314
33133315 if (result.ok) {
33143316 const data = await result.json();
33153317 return { format: 'jpg'data?.format, data: data?.image };
33163318 } else {
33173319 const text = await result.text();
33183320 throw new Error(text);
@@ -4757,7 +4759,7 @@ function isValidState() {
47574759 case sources.togetherai:
47584760 return secret_state[SECRET_KEYS.TOGETHERAI];
47594761 case sources.pollinations:
47604762 return truesecret_state[SECRET_KEYS.POLLINATIONS];
47614763 case sources.stability:
47624764 return secret_state[SECRET_KEYS.STABILITY];
47634765 case sources.huggingface:
@@ -5514,15 +5516,19 @@ jQuery(async () => {
55145516
55155517 [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => {
55165518 eventSource.on(event, async (/** @type {string} */ key) => {
55175519 switchconst (key)keySourceMap = {
55185520 case[sources.bfl]: SECRET_KEYS.BFL:,
55195521 case[sources.falai]: SECRET_KEYS.FALAI:,
55205522 case[sources.stability]: SECRET_KEYS.STABILITY:,
55215523 case[sources.aimlapi]: SECRET_KEYS.AIMLAPI:,
55225524 case[sources.comfy]: SECRET_KEYS.COMFY_RUNPOD:,
5523- await loadSettingOptions();
5525+ [sources.pollinations]: SECRET_KEYS.POLLINATIONS,
5524- break;
5526+ };
5527+ const shouldReloadOptions = Object.entries(keySourceMap).some(([k, v]) => k === extension_settings.sd.source && v === key);
5528+ if (!shouldReloadOptions) {
5529+ return;
55255530 }
5531+ await loadSettingOptions();
55265532 });
55275533 });
55285534
public/scripts/extensions/stable-diffusion/settings.html+8 -3
@@ -263,9 +263,14 @@
263263 </div>
264264 </div>
265265 <div data-sd-source="pollinations">
266- <p>
266+ <a href="https://enter.pollinations.ai">Pollinations.ai</a>
267- <a href="https://pollinations.ai">Pollinations.ai</a>
267+ <div class="flex-container flexnowrap alignItemsBaseline marginBot5">
268- </p>
268+ <strong class="flex1" data-i18n="API Key">API Key</strong>
269+ <div id="sd_pollinations_key" class="menu_button menu_button_icon manage-api-keys" data-key="api_key_pollinations">
270+ <i class="fa-fw fa-solid fa-key"></i>
271+ <span data-i18n="Click to set">Click to set</span>
272+ </div>
273+ </div>
269274 <div class="flex-container">
270275 <label class="flex1 checkbox_label" for="sd_pollinations_enhance" data-i18n="[title]Enables prompt enhancing (passes prompts through an LLM to add detail)." title="Enables prompt enhancing (passes prompts through an LLM to add detail).">
271276 <input id="sd_pollinations_enhance" type="checkbox" />
public/scripts/openai.js+2 -5
@@ -2707,10 +2707,6 @@ export async function createGenerationParameters(settings, model, type, messages
27072707 }
27082708 }
27092709
2710- if (settings.chat_completion_source === chat_completion_sources.POLLINATIONS) {
2711- delete generate_data.max_tokens;
2712- }
2713-
27142710 // https://docs.electronhub.ai/api-reference/chat/completions
27152711 if (settings.chat_completion_source === chat_completion_sources.ELECTRONHUB) {
27162712 generate_data.top_k = Number(settings.top_k_openai);
@@ -5608,6 +5604,7 @@ async function onConnectButtonClick(e) {
56085604 [chat_completion_sources.AZURE_OPENAI]: { key: SECRET_KEYS.AZURE_OPENAI, selector: '#api_key_azure_openai', proxy: false },
56095605 [chat_completion_sources.ZAI]: { key: SECRET_KEYS.ZAI, selector: '#api_key_zai', proxy: true },
56105606 [chat_completion_sources.CHUTES]: { key: SECRET_KEYS.CHUTES, selector: '#api_key_chutes', proxy: false },
5607+ [chat_completion_sources.POLLINATIONS]: { key: SECRET_KEYS.POLLINATIONS, selector: '#api_key_pollinations', proxy: false },
56115608 };
56125609
56135610 // Vertex AI Express version - use API key
@@ -5887,7 +5884,7 @@ export function isImageInliningSupported() {
58875884 case chat_completion_sources.ELECTRONHUB:
58885885 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.electronhub_model)?.metadata?.vision);
58895886 case chat_completion_sources.POLLINATIONS:
58905887 return (Array.isArray(model_list) && model_list.find(m => m.id === oai_settings.pollinations_model)?.visioninput_modalities?.includes('image'));
58915888 case chat_completion_sources.COMETAPI:
58925889 return true;
58935890 case chat_completion_sources.MOONSHOT:
public/scripts/secrets.js+3 -0
@@ -73,6 +73,7 @@ export const SECRET_KEYS = {
7373 ZAI: 'api_key_zai',
7474 SILICONFLOW: 'api_key_siliconflow',
7575 ELEVENLABS: 'api_key_elevenlabs',
76+ POLLINATIONS: 'api_key_pollinations',
7677};
7778
7879const FRIENDLY_NAMES = {
@@ -134,6 +135,7 @@ const FRIENDLY_NAMES = {
134135 [SECRET_KEYS.ZAI]: 'Z.AI',
135136 [SECRET_KEYS.SILICONFLOW]: 'SiliconFlow',
136137 [SECRET_KEYS.ELEVENLABS]: 'ElevenLabs TTS',
138+ [SECRET_KEYS.POLLINATIONS]: 'Pollinations',
137139};
138140
139141const INPUT_MAP = {
@@ -177,6 +179,7 @@ const INPUT_MAP = {
177179 [SECRET_KEYS.ZAI]: '#api_key_zai',
178180 [SECRET_KEYS.SILICONFLOW]: '#api_key_siliconflow',
179181 [SECRET_KEYS.COMFY_RUNPOD]: '#api_key_comfy_runpod',
182+ [SECRET_KEYS.POLLINATIONS]: '#api_key_pollinations',
180183};
181184
182185const getLabel = () => moment().format('L LT');
src/endpoints/backends/chat-completions.js+16 -12
@@ -79,7 +79,7 @@ const API_NANOGPT = 'https://nano-gpt.com/api/v1';
7979const API_DEEPSEEK = 'https://api.deepseek.com/beta';
8080const API_XAI = 'https://api.x.ai/v1';
8181const API_AIMLAPI = 'https://api.aimlapi.com/v1';
8282const API_POLLINATIONS = 'https://textgen.pollinations.ai/openaiv1';
8383const API_MOONSHOT = 'https://api.moonshot.ai/v1';
8484const API_FIREWORKS = 'https://api.fireworks.ai/inference/v1';
8585const API_COMETAPI = 'https://api.cometapi.com/v1';
@@ -1690,8 +1690,8 @@ router.post('/status', async function (request, statusResponse) {
16901690 apiKey = readSecret(request.user.directories, SECRET_KEYS.AIMLAPI);
16911691 headers = { ...AIMLAPI_HEADERS };
16921692 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
16931693 apiUrl = 'https://textgen.pollinations.ai/text';
1694- apiKey = 'NONE';
1694+ apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
16951695 headers = {};
16961696 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.GROQ) {
16971697 apiUrl = API_GROQ;
@@ -2244,18 +2244,19 @@ router.post('/generate', async function (request, response) {
22442244 }
22452245 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.POLLINATIONS) {
22462246 apiUrl = API_POLLINATIONS;
2247- apiKey = 'NONE';
2247+ apiKey = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
22482248 headers = {};
2249- 'Authorization': '',
2250- };
22512249 bodyParams = {
22522250 reasoning_effort: request.body.reasoning_effort,
2253- private: true,
2254- referrer: 'sillytavern',
22552251 seed: request.body.seed ?? Math.floor(Math.random() * 99999999),
22562252 };
22572253 if (request.body.json_schema) {
2258- setJsonObjectFormat(bodyParams, request.body.messages, request.body.json_schema);
2254+ bodyParams['response_format'] = {
2255+ type: 'json_schema',
2256+ json_schema: {
2257+ schema: request.body.json_schema.value,
2258+ },
2259+ };
22592260 }
22602261 } else if (request.body.chat_completion_source === CHAT_COMPLETION_SOURCES.MOONSHOT) {
22612262 apiUrl = new URL(request.body.reverse_proxy || API_MOONSHOT).toString();
@@ -2434,7 +2435,7 @@ const multimodalModels = express.Router();
24342435
24352436multimodalModels.post('/pollinations', async (_req, res) => {
24362437 try {
24372438 const response = await fetch('https://textgen.pollinations.ai/models');
24382439
24392440 if (!response.ok) {
24402441 return res.json([]);
@@ -2447,7 +2448,10 @@ multimodalModels.post('/pollinations', async (_req, res) => {
24472448 return res.json([]);
24482449 }
24492450
2450- const multimodalModels = data.filter(m => m?.vision).map(m => m.name);
2451+ const multimodalModels = data
2452+ .filter(m => Array.isArray(m?.input_modalities))
2453+ .filter(m => m.input_modalities.includes('image'))
2454+ .map(m => m.name);
24512455 return res.json(multimodalModels);
24522456 } catch (error) {
24532457 console.error(error);
src/endpoints/openai.js+7 -3
@@ -97,7 +97,12 @@ router.post('/caption-image', async (request, response) => {
9797 bodyParams.max_tokens = 4096; // default is 1024
9898 }
9999
100- const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp', 'pollinations'];
100+ if (request.body.api === 'pollinations') {
101+ key = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
102+ bodyParams.seed = Math.floor(Math.random() * Math.pow(2, 32));
103+ }
104+
105+ const noKeyTypes = ['custom', 'ooba', 'koboldcpp', 'vllm', 'llamacpp'];
101106 if (!key && !request.body.reverse_proxy && !noKeyTypes.includes(request.body.api)) {
102107 console.warn('No key found for API', request.body.api);
103108 return response.sendStatus(400);
@@ -173,8 +178,7 @@ router.post('/caption-image', async (request, response) => {
173178 }
174179
175180 if (request.body.api === 'pollinations') {
176- headers = { Authorization: '' };
181+ apiUrl = 'https://gen.pollinations.ai/v1/chat/completions';
177- apiUrl = 'https://text.pollinations.ai/openai/chat/completions';
178182 }
179183
180184 if (request.body.api === 'moonshot' && !request.body.reverse_proxy) {
src/endpoints/secrets.js+1 -0
@@ -66,6 +66,7 @@ export const SECRET_KEYS = {
6666 ZAI: 'api_key_zai',
6767 SILICONFLOW: 'api_key_siliconflow',
6868 ELEVENLABS: 'api_key_elevenlabs',
69+ POLLINATIONS: 'api_key_pollinations',
6970};
7071
7172/**
src/endpoints/speech.js+39 -8
@@ -89,7 +89,7 @@ pollinations.post('/voices', async (req, res) => {
8989 try {
9090 const model = req.body.model || 'openai-audio';
9191
9292 const response = await fetch('https://textgen.pollinations.ai/text/models');
9393
9494 if (!response.ok) {
9595 throw new Error('Failed to fetch Pollinations models');
@@ -116,25 +116,56 @@ pollinations.post('/voices', async (req, res) => {
116116
117117pollinations.post('/generate', async (req, res) => {
118118 try {
119+ const key = readSecret(req.user.directories, SECRET_KEYS.POLLINATIONS);
120+ if (!key) {
121+ console.warn('No API key saved for Pollinations TTS.');
122+ return res.sendStatus(400);
123+ }
124+
119125 const text = req.body.text;
120126 const model = req.body.model || 'openai-audio';
121127 const voice = req.body.voice || 'alloy';
122128
123- const url = new URL(`https://text.pollinations.ai/generate/${encodeURIComponent(text)}`);
129+ console.debug('Pollinations TTS request', { text, model, voice });
124- url.searchParams.append('model', model);
125- url.searchParams.append('voice', voice);
126- url.searchParams.append('referrer', 'sillytavern');
127- console.info('Pollinations request URL:', url.toString());
128130
129- const response = await fetch(url);
131+ const response = await fetch('https://gen.pollinations.ai/v1/chat/completions', {
132+ method: 'POST',
133+ headers: {
134+ 'Authorization': `Bearer ${key}`,
135+ 'Content-Type': 'application/json',
136+ },
137+ body: JSON.stringify({
138+ model: model,
139+ stream: false,
140+ modalities: ['text', 'audio'],
141+ seed: Math.floor(Math.random() * Math.pow(2, 32)),
142+ audio: {
143+ format: 'mp3',
144+ voice: voice,
145+ },
146+ messages: [{
147+ role: 'user',
148+ content: text,
149+ }],
150+ }),
151+ });
130152
131153 if (!response.ok) {
132154 const text = await response.text();
133155 throw new Error(`Failed to generate audio from Pollinations: ${text}`);
134156 }
135157
158+ /** @type {any} */
159+ const data = await response.json();
160+ const audioData = data?.choices?.[0]?.message?.audio?.data;
161+
162+ if (!audioData) {
163+ console.warn('Pollinations TTS audio data is missing from the response');
164+ return res.sendStatus(500);
165+ }
166+
136167 res.set('Content-Type', 'audio/mpeg');
137- forwardFetchResponse(response, res);
168+ return res.send(Buffer.from(audioData, 'base64'));
138169 } catch (error) {
139170 console.error(error);
140171 return res.sendStatus(500);
src/endpoints/stable-diffusion.js+18 -11
@@ -8,6 +8,7 @@ import { sync as writeFileAtomicSync } from 'write-file-atomic';
88import FormData from 'form-data';
99import urlJoin from 'url-join';
1010import _ from 'lodash';
11+import mime from 'mime-types';
1112
1213import { delay, getBasicAuthHeader, isValidUrl, tryParse } from '../util.js';
1314import { readSecret, SECRET_KEYS } from './secrets.js';
@@ -898,7 +899,7 @@ const pollinations = express.Router();
898899
899900pollinations.post('/models', async (_request, response) => {
900901 try {
901902 const modelsUrl = new URL('https://imagegen.pollinations.ai/image/models');
902903 const result = await fetch(modelsUrl);
903904
904905 if (!result.ok) {
@@ -913,7 +914,7 @@ pollinations.post('/models', async (_request, response) => {
913914 throw new Error('Pollinations request failed.');
914915 }
915916
916917 const models = data.map(x => ({ value: x.name, text: x.name }));
917918 return response.send(models);
918919 } catch (error) {
919920 console.error(error);
@@ -923,17 +924,19 @@ pollinations.post('/models', async (_request, response) => {
923924
924925pollinations.post('/generate', async (request, response) => {
925926 try {
926- const promptUrl = new URL(`https://image.pollinations.ai/prompt/${encodeURIComponent(request.body.prompt)}`);
927+ const key = readSecret(request.user.directories, SECRET_KEYS.POLLINATIONS);
928+ if (!key) {
929+ console.warn('Pollinations API key not found.');
930+ return response.sendStatus(400);
931+ }
932+
933+ const promptUrl = new URL(`https://gen.pollinations.ai/image/${encodeURIComponent(request.body.prompt)}`);
927934 const params = new URLSearchParams({
928935 model: String(request.body.model),
929936 negative_prompt: String(request.body.negative_prompt),
930937 seed: String(request.body.seed >= 0 ? request.body.seed : Math.floor(Math.random() * 10_000_000)),
931938 width: String(request.body.width ?? 1024),
932939 height: String(request.body.height ?? 1024),
933- nologo: String(true),
934- nofeed: String(true),
935- private: String(true),
936- referrer: 'sillytavern',
937940 });
938941 if (request.body.enhance) {
939942 params.set('enhance', String(true));
@@ -942,7 +945,12 @@ pollinations.post('/generate', async (request, response) => {
942945
943946 console.info('Pollinations request URL:', promptUrl.toString());
944947
945948 const result = await fetch(promptUrl);, {
949+ method: 'GET',
950+ headers: {
951+ 'Authorization': `Bearer ${key}`,
952+ },
953+ });
946954
947955 if (!result.ok) {
948956 const text = await result.text();
@@ -950,10 +958,9 @@ pollinations.post('/generate', async (request, response) => {
950958 throw new Error('Pollinations request failed.');
951959 }
952960
961+ const format = result.headers.get('Content-Type')?.toString() || 'image/jpeg';
953962 const buffer = await result.arrayBuffer();
954963 constreturn base64response.send({ =image: Buffer.from(buffer).toString('base64'), format: mime.extension(format) || 'jpg' });
955-
956- return response.send({ image: base64 });
957964 } catch (error) {
958965 console.error(error);
959966 return response.sendStatus(500);