From 0a84605bacf94c3b7aebf21a75085a2d2a8203e4 Mon Sep 17 00:00:00 2001
From: Hackall <36754621+hackall360@users.noreply.github.com>
Date: Wed, 17 Sep 2025 05:43:36 -0700
Subject: [PATCH 1/3] Allow running without Pollinations token
---
README.md | 4 +-
src/main.js | 22 ++++-
src/pollinations-client.js | 74 ++++++++------
tests/pollinations-token-optional.test.mjs | 108 +++++++++++++++++++++
4 files changed, 178 insertions(+), 30 deletions(-)
create mode 100644 tests/pollinations-token-optional.test.mjs
diff --git a/README.md b/README.md
index 8fba584..15a1075 100644
--- a/README.md
+++ b/README.md
@@ -47,4 +47,6 @@ expects the token to be provided at runtime so it is never bundled into the stat
will capture the token, remove it from the visible URL, and apply it to subsequent Pollinations
requests.
-If the token cannot be resolved the UI remains disabled and an error is shown in the status banner.
+If the token cannot be resolved the application continues without one so you can still browse public
+models. A warning is shown to indicate that gated Pollinations models will remain unavailable until a
+token is supplied.
diff --git a/src/main.js b/src/main.js
index 5b2a636..65ec228 100644
--- a/src/main.js
+++ b/src/main.js
@@ -907,11 +907,27 @@ async function initializeApp() {
els.voicePlayback.checked = false;
}
+ let tokenSource = null;
+ let tokenMessages = [];
+
try {
- const { client: polliClient, tokenSource } = await createPollinationsClient();
+ const {
+ client: polliClient,
+ tokenSource: resolvedTokenSource,
+ tokenMessages: resolvedTokenMessages,
+ } = await createPollinationsClient();
client = polliClient;
+ tokenSource = resolvedTokenSource;
+ tokenMessages = Array.isArray(resolvedTokenMessages) ? resolvedTokenMessages : [];
if (tokenSource) {
console.info('Pollinations token loaded via %s.', tokenSource);
+ } else if (tokenMessages.length) {
+ console.warn(
+ 'Proceeding without a Pollinations token. Attempts: %s',
+ tokenMessages.join('; '),
+ );
+ } else {
+ console.info('Proceeding without a Pollinations token.');
}
} catch (error) {
console.error('Failed to configure Pollinations client', error);
@@ -928,6 +944,10 @@ async function initializeApp() {
setLoading(false);
}
+ if (!tokenSource && !state.statusError) {
+ setStatus('Ready. Pollinations token not configured; only public models are available.');
+ }
+
try {
setupRecognition();
} catch (error) {
diff --git a/src/pollinations-client.js b/src/pollinations-client.js
index ea4db81..95e34d7 100644
--- a/src/pollinations-client.js
+++ b/src/pollinations-client.js
@@ -1,34 +1,49 @@
import { PolliClient } from '../Libs/pollilib/index.js';
let tokenPromise = null;
-let cachedToken = null;
-let cachedSource = null;
+let cachedResult = null;
export async function createPollinationsClient({ referrer } = {}) {
- const { token, source } = await ensureToken();
- const getToken = async () => token;
- const client = new PolliClient({
- auth: {
+ const tokenResult = await ensureToken();
+ const { token, source, messages = [], errors = [] } = tokenResult;
+ const inferredReferrer = referrer ?? inferReferrer();
+
+ const clientOptions = {};
+ if (token) {
+ clientOptions.auth = {
mode: 'token',
placement: 'query',
- getToken,
- referrer: referrer ?? inferReferrer(),
- },
- });
- return { client, tokenSource: source };
+ getToken: async () => token,
+ referrer: inferredReferrer ?? undefined,
+ };
+ } else if (inferredReferrer) {
+ clientOptions.referrer = inferredReferrer;
+ }
+
+ const client = new PolliClient(clientOptions);
+ return {
+ client,
+ tokenSource: token ? source : null,
+ tokenMessages: messages,
+ tokenErrors: errors,
+ };
}
async function ensureToken() {
- if (cachedToken) {
- return { token: cachedToken, source: cachedSource };
+ if (cachedResult) {
+ return cachedResult;
}
if (!tokenPromise) {
- tokenPromise = resolveToken();
- }
- const result = await tokenPromise;
- cachedToken = result.token;
- cachedSource = result.source;
- return result;
+ tokenPromise = resolveToken()
+ .then(result => {
+ cachedResult = result;
+ return result;
+ })
+ .finally(() => {
+ tokenPromise = null;
+ });
+ }
+ return tokenPromise;
}
async function resolveToken() {
@@ -45,9 +60,14 @@ async function resolveToken() {
try {
const result = await attempt();
if (result?.token) {
+ const messages = errors
+ .map(entry => formatError(entry.source, entry.error))
+ .filter(Boolean);
return {
token: result.token,
source: result.source ?? attempt.name ?? 'unknown',
+ errors,
+ messages,
};
}
if (result?.error) {
@@ -61,13 +81,12 @@ async function resolveToken() {
const messages = errors
.map(entry => formatError(entry.source, entry.error))
.filter(Boolean);
- const message =
- messages.length > 0
- ? `Unable to load Pollinations token. Attempts: ${messages.join('; ')}`
- : 'Unable to load Pollinations token.';
- const failure = new Error(message);
- failure.causes = errors;
- throw failure;
+ return {
+ token: null,
+ source: null,
+ errors,
+ messages,
+ };
}
async function fetchTokenFromApi() {
@@ -429,8 +448,7 @@ function inferReferrer() {
function resetTokenCache() {
tokenPromise = null;
- cachedToken = null;
- cachedSource = null;
+ cachedResult = null;
}
export const __testing = {
diff --git a/tests/pollinations-token-optional.test.mjs b/tests/pollinations-token-optional.test.mjs
new file mode 100644
index 0000000..b2f7c0e
--- /dev/null
+++ b/tests/pollinations-token-optional.test.mjs
@@ -0,0 +1,108 @@
+import assert from 'node:assert/strict';
+import { createPollinationsClient, __testing } from '../src/pollinations-client.js';
+
+export const name = 'Pollinations client falls back to unauthenticated access when no token is available';
+
+function createStubResponse(status = 404) {
+ return {
+ status,
+ ok: status >= 200 && status < 300,
+ headers: {
+ get() {
+ return null;
+ },
+ },
+ async json() {
+ return {};
+ },
+ async text() {
+ return '';
+ },
+ };
+}
+
+export async function run() {
+ const originalFetch = globalThis.fetch;
+ const originalWindow = globalThis.window;
+ const originalDocument = globalThis.document;
+ const originalLocation = globalThis.location;
+ const originalHistory = globalThis.history;
+ const originalNodeEnv = process.env.NODE_ENV;
+ const envKeys = [
+ 'POLLI_TOKEN',
+ 'VITE_POLLI_TOKEN',
+ 'POLLINATIONS_TOKEN',
+ 'VITE_POLLINATIONS_TOKEN',
+ ];
+ const originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]));
+
+ try {
+ globalThis.fetch = async () => createStubResponse(404);
+ delete globalThis.window;
+ delete globalThis.document;
+ delete globalThis.location;
+ delete globalThis.history;
+ for (const key of envKeys) {
+ delete process.env[key];
+ }
+ delete process.env.NODE_ENV;
+
+ __testing.resetTokenCache();
+
+ const { client, tokenSource, tokenMessages } = await createPollinationsClient();
+
+ assert.equal(tokenSource, null);
+ assert.equal(client.authMode, 'none');
+ assert.ok(Array.isArray(tokenMessages));
+ assert.ok(
+ tokenMessages.some(message => message && message.includes('Token endpoint not found')),
+ 'tokenMessages should include the failed API attempt',
+ );
+ } finally {
+ if (originalFetch) {
+ globalThis.fetch = originalFetch;
+ } else {
+ delete globalThis.fetch;
+ }
+
+ if (typeof originalWindow === 'undefined') {
+ delete globalThis.window;
+ } else {
+ globalThis.window = originalWindow;
+ }
+
+ if (typeof originalDocument === 'undefined') {
+ delete globalThis.document;
+ } else {
+ globalThis.document = originalDocument;
+ }
+
+ if (typeof originalLocation === 'undefined') {
+ delete globalThis.location;
+ } else {
+ globalThis.location = originalLocation;
+ }
+
+ if (typeof originalHistory === 'undefined') {
+ delete globalThis.history;
+ } else {
+ globalThis.history = originalHistory;
+ }
+
+ for (const key of envKeys) {
+ if (typeof originalEnv[key] === 'undefined') {
+ delete process.env[key];
+ } else {
+ process.env[key] = originalEnv[key];
+ }
+ }
+
+ if (typeof originalNodeEnv === 'undefined') {
+ delete process.env.NODE_ENV;
+ } else {
+ process.env.NODE_ENV = originalNodeEnv;
+ }
+
+ __testing.resetTokenCache();
+ }
+}
From d21e883aa42ea9af7d5995edbdf6e54b5e122819 Mon Sep 17 00:00:00 2001
From: Hackall <36754621+hackall360@users.noreply.github.com>
Date: Wed, 17 Sep 2025 06:15:13 -0700
Subject: [PATCH 2/3] Make Pollinations token endpoint optional
---
README.md | 31 ++++++-----
src/pollinations-client.js | 57 +++++++++++++++++--
tests/pollinations-token-optional.test.mjs | 64 ++++++++++++++++++----
3 files changed, 123 insertions(+), 29 deletions(-)
diff --git a/README.md b/README.md
index 15a1075..5f60a5a 100644
--- a/README.md
+++ b/README.md
@@ -33,20 +33,21 @@ make sure the contents of `dist/` are deployed.
## Configuring the Pollinations token
-Pollinations models that require tiered access need a token on every request. The application now
-expects the token to be provided at runtime so it is never bundled into the static assets.
+Pollinations models that require tiered access expect the token to be supplied as a request
+parameter. The demo resolves the token at runtime so secrets are never baked into the static assets.
- **GitHub Pages / production** – Provide the `POLLI_TOKEN` secret in the repository (or Pages
- environment). The included Pages Function at `.github/functions/polli-token.js` exposes the token
- at runtime via `/api/polli-token`, and responses are marked as non-cacheable.
-- **Local development** – Either define `POLLI_TOKEN`/`VITE_POLLI_TOKEN` in your shell when running
- `npm run dev`, add a `` tag to `index.html`, or inject
- `window.__POLLINATIONS_TOKEN__` before the application bootstraps.
-- **Static overrides** – When a dynamic endpoint is unavailable, append a `token` query parameter
- to the page URL (e.g. `https://example.github.io/chatdemo/?token=your-secret`). The application
- will capture the token, remove it from the visible URL, and apply it to subsequent Pollinations
- requests.
-
-If the token cannot be resolved the application continues without one so you can still browse public
-models. A warning is shown to indicate that gated Pollinations models will remain unavailable until a
-token is supplied.
+ environment). You can surface the token to the client by setting `window.__POLLINATIONS_TOKEN__`,
+ defining a `` tag, or adding a `token=...` query
+ parameter to the published URL (e.g. `https://example.github.io/chatdemo/?token=your-secret`). The
+ token is removed from the visible URL after it is captured.
+- **Local development** – Define `POLLI_TOKEN`/`VITE_POLLI_TOKEN` in your shell when running
+ `npm run dev`, add a meta tag as above, or inject `window.__POLLINATIONS_TOKEN__` before the
+ application bootstraps.
+- **Optional runtime endpoint** – If you expose the token via a custom endpoint, configure its URL
+ with `POLLI_TOKEN_ENDPOINT`/`VITE_POLLI_TOKEN_ENDPOINT` (environment variables),
+ `window.__POLLINATIONS_TOKEN_ENDPOINT__`, or a `` tag.
+ When present, the client will fetch the token from that endpoint.
+
+If the token cannot be resolved the application continues without one, allowing you to browse public
+models while gated Pollinations models remain unavailable until a token is supplied.
diff --git a/src/pollinations-client.js b/src/pollinations-client.js
index 95e34d7..68fe8f5 100644
--- a/src/pollinations-client.js
+++ b/src/pollinations-client.js
@@ -90,11 +90,15 @@ async function resolveToken() {
}
async function fetchTokenFromApi() {
+ const endpoint = resolveTokenEndpoint();
+ if (!endpoint) {
+ return { token: null, source: 'api' };
+ }
if (typeof fetch !== 'function') {
return { token: null, source: 'api', error: new Error('Fetch is unavailable in this environment.') };
}
try {
- const response = await fetch('/api/polli-token', {
+ const response = await fetch(endpoint, {
method: 'GET',
headers: { Accept: 'application/json' },
cache: 'no-store',
@@ -129,7 +133,7 @@ async function fetchTokenFromApi() {
function readTokenFromUrl() {
const location = getCurrentLocation();
if (!location) {
- return { token: null, source: 'url', error: new Error('Location is unavailable.') };
+ return { token: null, source: 'url' };
}
const { url, searchParams, hashParams, rawFragments } = parseLocation(location);
@@ -162,7 +166,7 @@ function readTokenFromUrl() {
function readTokenFromMeta() {
if (typeof document === 'undefined') {
- return { token: null, source: 'meta', error: new Error('Document is unavailable.') };
+ return { token: null, source: 'meta' };
}
const meta = document.querySelector('meta[name="pollinations-token"]');
if (!meta) {
@@ -183,7 +187,7 @@ function readTokenFromMeta() {
function readTokenFromWindow() {
if (typeof window === 'undefined') {
- return { token: null, source: 'window', error: new Error('Window is unavailable.') };
+ return { token: null, source: 'window' };
}
const candidate = window.__POLLINATIONS_TOKEN__ ?? window.POLLI_TOKEN ?? null;
const token = extractTokenValue(candidate);
@@ -394,6 +398,51 @@ function determineDevelopmentEnvironment(importMetaEnv, processEnv) {
return false;
}
+function resolveTokenEndpoint() {
+ const importMetaEnv = typeof import.meta !== 'undefined' ? import.meta.env ?? undefined : undefined;
+ const processEnv = typeof process !== 'undefined' && process?.env ? process.env : undefined;
+ const envCandidates = [
+ importMetaEnv?.VITE_POLLI_TOKEN_ENDPOINT,
+ importMetaEnv?.POLLI_TOKEN_ENDPOINT,
+ importMetaEnv?.VITE_POLLINATIONS_TOKEN_ENDPOINT,
+ importMetaEnv?.POLLINATIONS_TOKEN_ENDPOINT,
+ processEnv?.VITE_POLLI_TOKEN_ENDPOINT,
+ processEnv?.POLLI_TOKEN_ENDPOINT,
+ processEnv?.VITE_POLLINATIONS_TOKEN_ENDPOINT,
+ processEnv?.POLLINATIONS_TOKEN_ENDPOINT,
+ ];
+
+ const windowCandidates = [];
+ if (typeof window !== 'undefined') {
+ windowCandidates.push(
+ window.__POLLINATIONS_TOKEN_ENDPOINT__,
+ window.POLLI_TOKEN_ENDPOINT,
+ window.POLLINATIONS_TOKEN_ENDPOINT,
+ );
+ }
+
+ const metaCandidates = [];
+ if (typeof document !== 'undefined' && document?.querySelector) {
+ const names = ['pollinations-token-endpoint', 'polli-token-endpoint'];
+ for (const name of names) {
+ const meta = document.querySelector(`meta[name="${name}"]`);
+ if (!meta) continue;
+ const content = meta.getAttribute('content');
+ if (typeof content === 'string') {
+ metaCandidates.push(content);
+ }
+ }
+ }
+
+ const candidates = [...envCandidates, ...windowCandidates, ...metaCandidates];
+ for (const candidate of candidates) {
+ if (typeof candidate !== 'string') continue;
+ const trimmed = candidate.trim();
+ if (trimmed) return trimmed;
+ }
+ return null;
+}
+
function extractTokenValue(value) {
if (value == null) return null;
if (typeof value === 'string') {
diff --git a/tests/pollinations-token-optional.test.mjs b/tests/pollinations-token-optional.test.mjs
index b2f7c0e..6ae2cc8 100644
--- a/tests/pollinations-token-optional.test.mjs
+++ b/tests/pollinations-token-optional.test.mjs
@@ -1,7 +1,8 @@
import assert from 'node:assert/strict';
import { createPollinationsClient, __testing } from '../src/pollinations-client.js';
-export const name = 'Pollinations client falls back to unauthenticated access when no token is available';
+export const name =
+ 'Pollinations client falls back to unauthenticated access when no token endpoint is configured';
function createStubResponse(status = 404) {
return {
@@ -28,24 +29,49 @@ export async function run() {
const originalLocation = globalThis.location;
const originalHistory = globalThis.history;
const originalNodeEnv = process.env.NODE_ENV;
- const envKeys = [
+ const originalGlobalEndpoints = {
+ __POLLINATIONS_TOKEN_ENDPOINT__: globalThis.__POLLINATIONS_TOKEN_ENDPOINT__,
+ POLLI_TOKEN_ENDPOINT: globalThis.POLLI_TOKEN_ENDPOINT,
+ POLLINATIONS_TOKEN_ENDPOINT: globalThis.POLLINATIONS_TOKEN_ENDPOINT,
+ };
+ const tokenEnvKeys = [
'POLLI_TOKEN',
'VITE_POLLI_TOKEN',
'POLLINATIONS_TOKEN',
'VITE_POLLINATIONS_TOKEN',
];
- const originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]));
+ const endpointEnvKeys = [
+ 'POLLI_TOKEN_ENDPOINT',
+ 'VITE_POLLI_TOKEN_ENDPOINT',
+ 'POLLINATIONS_TOKEN_ENDPOINT',
+ 'VITE_POLLINATIONS_TOKEN_ENDPOINT',
+ ];
+ const originalEnv = Object.fromEntries(
+ [...tokenEnvKeys, ...endpointEnvKeys].map(key => [key, process.env[key]]),
+ );
try {
- globalThis.fetch = async () => createStubResponse(404);
+ let fetchCalled = 0;
+ const fetchUrls = [];
+ globalThis.fetch = async (...args) => {
+ fetchCalled += 1;
+ fetchUrls.push(args[0]);
+ return createStubResponse(404);
+ };
delete globalThis.window;
delete globalThis.document;
delete globalThis.location;
delete globalThis.history;
- for (const key of envKeys) {
+ for (const key of tokenEnvKeys) {
+ delete process.env[key];
+ }
+ for (const key of endpointEnvKeys) {
delete process.env[key];
}
delete process.env.NODE_ENV;
+ delete globalThis.__POLLINATIONS_TOKEN_ENDPOINT__;
+ delete globalThis.POLLI_TOKEN_ENDPOINT;
+ delete globalThis.POLLINATIONS_TOKEN_ENDPOINT;
__testing.resetTokenCache();
@@ -54,10 +80,10 @@ export async function run() {
assert.equal(tokenSource, null);
assert.equal(client.authMode, 'none');
assert.ok(Array.isArray(tokenMessages));
- assert.ok(
- tokenMessages.some(message => message && message.includes('Token endpoint not found')),
- 'tokenMessages should include the failed API attempt',
- );
+ assert.equal(tokenMessages.length, 0, `Unexpected messages: ${tokenMessages.join('; ')}`);
+ if (fetchCalled !== 0) {
+ throw new Error(`Unexpected token fetch attempts: ${fetchUrls.join(', ')}`);
+ }
} finally {
if (originalFetch) {
globalThis.fetch = originalFetch;
@@ -89,7 +115,7 @@ export async function run() {
globalThis.history = originalHistory;
}
- for (const key of envKeys) {
+ for (const key of [...tokenEnvKeys, ...endpointEnvKeys]) {
if (typeof originalEnv[key] === 'undefined') {
delete process.env[key];
} else {
@@ -103,6 +129,24 @@ export async function run() {
process.env.NODE_ENV = originalNodeEnv;
}
+ if (typeof originalGlobalEndpoints.__POLLINATIONS_TOKEN_ENDPOINT__ === 'undefined') {
+ delete globalThis.__POLLINATIONS_TOKEN_ENDPOINT__;
+ } else {
+ globalThis.__POLLINATIONS_TOKEN_ENDPOINT__ =
+ originalGlobalEndpoints.__POLLINATIONS_TOKEN_ENDPOINT__;
+ }
+ if (typeof originalGlobalEndpoints.POLLI_TOKEN_ENDPOINT === 'undefined') {
+ delete globalThis.POLLI_TOKEN_ENDPOINT;
+ } else {
+ globalThis.POLLI_TOKEN_ENDPOINT = originalGlobalEndpoints.POLLI_TOKEN_ENDPOINT;
+ }
+ if (typeof originalGlobalEndpoints.POLLINATIONS_TOKEN_ENDPOINT === 'undefined') {
+ delete globalThis.POLLINATIONS_TOKEN_ENDPOINT;
+ } else {
+ globalThis.POLLINATIONS_TOKEN_ENDPOINT =
+ originalGlobalEndpoints.POLLINATIONS_TOKEN_ENDPOINT;
+ }
+
__testing.resetTokenCache();
}
}
From 551c62897423b6a7c679283900062a32551518b4 Mon Sep 17 00:00:00 2001
From: Hackall <36754621+hackall360@users.noreply.github.com>
Date: Wed, 17 Sep 2025 09:40:51 -0700
Subject: [PATCH 3/3] Ensure Pollinations token envs and seeded requests
---
README.md | 16 ++++--
src/main.js | 14 +++--
src/pollinations-client.js | 62 ++++++++++------------
src/seed.js | 10 ++++
tests/pollinations-token-env.test.mjs | 24 ++++++---
tests/pollinations-token-optional.test.mjs | 2 +
tests/seed-generator.test.mjs | 23 ++++++++
7 files changed, 103 insertions(+), 48 deletions(-)
create mode 100644 src/seed.js
create mode 100644 tests/seed-generator.test.mjs
diff --git a/README.md b/README.md
index 5f60a5a..b269785 100644
--- a/README.md
+++ b/README.md
@@ -34,16 +34,19 @@ make sure the contents of `dist/` are deployed.
## Configuring the Pollinations token
Pollinations models that require tiered access expect the token to be supplied as a request
-parameter. The demo resolves the token at runtime so secrets are never baked into the static assets.
+parameter. The demo can resolve the token at runtime (via URL parameters, meta tags, or injected
+globals) and also honours build-time environment variables when you want to bake the token into the
+bundle.
- **GitHub Pages / production** – Provide the `POLLI_TOKEN` secret in the repository (or Pages
environment). You can surface the token to the client by setting `window.__POLLINATIONS_TOKEN__`,
- defining a `` tag, or adding a `token=...` query
- parameter to the published URL (e.g. `https://example.github.io/chatdemo/?token=your-secret`). The
- token is removed from the visible URL after it is captured.
+ defining a `` tag, adding a `token=...` query
+ parameter to the published URL (e.g. `https://example.github.io/chatdemo/?token=your-secret`), or
+ injecting `POLLI_TOKEN`/`VITE_POLLI_TOKEN` during the build so the token ships with the bundle.
+ The token is removed from the visible URL after it is captured.
- **Local development** – Define `POLLI_TOKEN`/`VITE_POLLI_TOKEN` in your shell when running
`npm run dev`, add a meta tag as above, or inject `window.__POLLINATIONS_TOKEN__` before the
- application bootstraps.
+ application bootstraps. Build-time environment variables also work in development.
- **Optional runtime endpoint** – If you expose the token via a custom endpoint, configure its URL
with `POLLI_TOKEN_ENDPOINT`/`VITE_POLLI_TOKEN_ENDPOINT` (environment variables),
`window.__POLLINATIONS_TOKEN_ENDPOINT__`, or a `` tag.
@@ -51,3 +54,6 @@ parameter. The demo resolves the token at runtime so secrets are never baked int
If the token cannot be resolved the application continues without one, allowing you to browse public
models while gated Pollinations models remain unavailable until a token is supplied.
+
+All chat and image requests automatically include a random eight-digit `seed` parameter so they
+match Pollinations' expected request format.
diff --git a/src/main.js b/src/main.js
index 65ec228..01cf6b9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -6,6 +6,7 @@ import {
matchesModelIdentifier,
normalizeTextCatalog,
} from './model-catalog.js';
+import { generateSeed } from './seed.js';
const FALLBACK_MODELS = [
createFallbackModel('openai', 'OpenAI GPT-5 Nano (fallback)'),
@@ -443,6 +444,7 @@ async function handleChatResponse(initialResponse, model, endpoint) {
messages: state.conversation,
tools: [IMAGE_TOOL],
tool_choice: 'auto',
+ seed: generateSeed(),
},
client,
);
@@ -499,7 +501,7 @@ async function handleToolCalls(toolCalls) {
const caption = String(args.caption ?? prompt).trim() || prompt;
try {
- const { dataUrl } = await generateImageAsset(prompt, {
+ const { dataUrl, seed } = await generateImageAsset(prompt, {
width,
height,
model: args.model,
@@ -520,6 +522,7 @@ async function handleToolCalls(toolCalls) {
prompt,
width,
height,
+ seed,
}),
});
} catch (error) {
@@ -545,12 +548,14 @@ async function generateImageAsset(prompt, { width, height, model: imageModel } =
if (!client) {
throw new Error('Pollinations client is not ready.');
}
+ const seed = generateSeed();
const binary = await image(
prompt,
{
width,
height,
model: imageModel,
+ seed,
nologo: true,
private: true,
enhance: true,
@@ -559,7 +564,7 @@ async function generateImageAsset(prompt, { width, height, model: imageModel } =
);
const dataUrl = binary.toDataUrl();
resetStatusIfIdle();
- return { dataUrl };
+ return { dataUrl, seed };
} catch (error) {
console.error('Image generation failed', error);
throw error;
@@ -770,6 +775,7 @@ async function requestChatCompletion(model, endpoints) {
const attemptErrors = [];
for (const endpoint of endpoints) {
try {
+ const requestSeed = generateSeed();
const response = await chat(
{
model: model.id,
@@ -777,6 +783,7 @@ async function requestChatCompletion(model, endpoints) {
messages: state.conversation,
tools: [IMAGE_TOOL],
tool_choice: 'auto',
+ seed: requestSeed,
},
client,
);
@@ -975,7 +982,7 @@ els.form.addEventListener('submit', async event => {
if (!prompt) {
throw new Error('Provide a prompt after /image');
}
- const { dataUrl } = await generateImageAsset(prompt);
+ const { dataUrl, seed } = await generateImageAsset(prompt);
addMessage({
role: 'assistant',
type: 'image',
@@ -983,6 +990,7 @@ els.form.addEventListener('submit', async event => {
alt: prompt,
caption: prompt,
});
+ console.info('Generated Pollinations image with seed %s.', seed);
resetStatusIfIdle();
} else {
await sendPrompt(raw);
diff --git a/src/pollinations-client.js b/src/pollinations-client.js
index 68fe8f5..d3dce8c 100644
--- a/src/pollinations-client.js
+++ b/src/pollinations-client.js
@@ -211,26 +211,32 @@ function readTokenFromEnv() {
const importMetaEnv = typeof import.meta !== 'undefined' ? import.meta.env ?? undefined : undefined;
const processEnv = typeof process !== 'undefined' && process?.env ? process.env : undefined;
- const isDev = determineDevelopmentEnvironment(importMetaEnv, processEnv);
- if (!isDev) {
- return { token: null, source: 'env' };
- }
-
- const token = extractTokenValue([
- importMetaEnv?.VITE_POLLI_TOKEN,
- importMetaEnv?.POLLI_TOKEN,
- importMetaEnv?.VITE_POLLINATIONS_TOKEN,
- importMetaEnv?.POLLINATIONS_TOKEN,
- processEnv?.VITE_POLLI_TOKEN,
- processEnv?.POLLI_TOKEN,
- processEnv?.VITE_POLLINATIONS_TOKEN,
- processEnv?.POLLINATIONS_TOKEN,
- ]);
+ const sources = [];
+ if (importMetaEnv) {
+ sources.push([
+ importMetaEnv.VITE_POLLI_TOKEN,
+ importMetaEnv.POLLI_TOKEN,
+ importMetaEnv.VITE_POLLINATIONS_TOKEN,
+ importMetaEnv.POLLINATIONS_TOKEN,
+ ]);
+ }
+ if (processEnv) {
+ sources.push([
+ processEnv.VITE_POLLI_TOKEN,
+ processEnv.POLLI_TOKEN,
+ processEnv.VITE_POLLINATIONS_TOKEN,
+ processEnv.POLLINATIONS_TOKEN,
+ ]);
+ }
- if (!token) {
- return { token: null, source: 'env' };
+ for (const group of sources) {
+ const token = extractTokenValue(group);
+ if (token) {
+ return { token, source: 'env' };
+ }
}
- return { token, source: 'env' };
+
+ return { token: null, source: 'env' };
}
function getCurrentLocation() {
@@ -383,21 +389,6 @@ function sanitizeUrlToken(location, url, tokenKeys) {
}
}
-function determineDevelopmentEnvironment(importMetaEnv, processEnv) {
- if (importMetaEnv && typeof importMetaEnv.DEV !== 'undefined') {
- return !!importMetaEnv.DEV;
- }
- if (processEnv) {
- if (typeof processEnv.VITE_DEV_SERVER_URL !== 'undefined') {
- return true;
- }
- if (typeof processEnv.NODE_ENV !== 'undefined') {
- return processEnv.NODE_ENV !== 'production';
- }
- }
- return false;
-}
-
function resolveTokenEndpoint() {
const importMetaEnv = typeof import.meta !== 'undefined' ? import.meta.env ?? undefined : undefined;
const processEnv = typeof process !== 'undefined' && process?.env ? process.env : undefined;
@@ -448,6 +439,10 @@ function extractTokenValue(value) {
if (typeof value === 'string') {
const trimmed = value.trim();
if (!trimmed) return null;
+ const lower = trimmed.toLowerCase();
+ if (lower === 'undefined' || lower === 'null') {
+ return null;
+ }
if ((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']'))) {
try {
return extractTokenValue(JSON.parse(trimmed));
@@ -502,5 +497,4 @@ function resetTokenCache() {
export const __testing = {
resetTokenCache,
- determineDevelopmentEnvironment,
};
diff --git a/src/seed.js b/src/seed.js
new file mode 100644
index 0000000..f48073f
--- /dev/null
+++ b/src/seed.js
@@ -0,0 +1,10 @@
+const MIN_SEED = 10_000_000;
+const MAX_SEED = 99_999_999;
+
+export function generateSeed(random = Math.random) {
+ const fn = typeof random === 'function' ? random : Math.random;
+ const value = fn();
+ const clamped = Number.isFinite(value) ? Math.max(0, Math.min(0.999999999999, value)) : 0;
+ const span = MAX_SEED - MIN_SEED + 1;
+ return Math.floor(clamped * span + MIN_SEED);
+}
diff --git a/tests/pollinations-token-env.test.mjs b/tests/pollinations-token-env.test.mjs
index 9713e1b..81ae803 100644
--- a/tests/pollinations-token-env.test.mjs
+++ b/tests/pollinations-token-env.test.mjs
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import { createPollinationsClient, __testing } from '../src/pollinations-client.js';
-export const name = 'Pollinations client resolves tokens from development environment variables';
+export const name = 'Pollinations client resolves tokens from environment variables';
function createStubResponse(status = 404) {
return {
@@ -24,12 +24,16 @@ function createStubResponse(status = 404) {
export async function run() {
const originalFetch = globalThis.fetch;
const originalToken = process.env.POLLI_TOKEN;
+ const originalViteToken = process.env.VITE_POLLI_TOKEN;
+ const originalVitePollinationsToken = process.env.VITE_POLLINATIONS_TOKEN;
const originalNodeEnv = process.env.NODE_ENV;
try {
globalThis.fetch = async () => createStubResponse(404);
process.env.POLLI_TOKEN = 'process-env-token';
- process.env.NODE_ENV = 'development';
+ process.env.VITE_POLLI_TOKEN = 'undefined';
+ process.env.VITE_POLLINATIONS_TOKEN = 'null';
+ process.env.NODE_ENV = 'production';
__testing.resetTokenCache();
const { client, tokenSource } = await createPollinationsClient();
@@ -50,10 +54,18 @@ export async function run() {
process.env.POLLI_TOKEN = originalToken;
}
- if (typeof originalNodeEnv === 'undefined') {
- delete process.env.NODE_ENV;
- } else {
- process.env.NODE_ENV = originalNodeEnv;
+ const envKeys = [
+ ['POLLI_TOKEN', originalToken],
+ ['VITE_POLLI_TOKEN', originalViteToken],
+ ['VITE_POLLINATIONS_TOKEN', originalVitePollinationsToken],
+ ['NODE_ENV', originalNodeEnv],
+ ];
+ for (const [key, original] of envKeys) {
+ if (typeof original === 'undefined') {
+ delete process.env[key];
+ } else {
+ process.env[key] = original;
+ }
}
__testing.resetTokenCache();
diff --git a/tests/pollinations-token-optional.test.mjs b/tests/pollinations-token-optional.test.mjs
index 6ae2cc8..5247335 100644
--- a/tests/pollinations-token-optional.test.mjs
+++ b/tests/pollinations-token-optional.test.mjs
@@ -68,6 +68,8 @@ export async function run() {
for (const key of endpointEnvKeys) {
delete process.env[key];
}
+ process.env.POLLI_TOKEN = 'undefined';
+ process.env.VITE_POLLI_TOKEN = 'null';
delete process.env.NODE_ENV;
delete globalThis.__POLLINATIONS_TOKEN_ENDPOINT__;
delete globalThis.POLLI_TOKEN_ENDPOINT;
diff --git a/tests/seed-generator.test.mjs b/tests/seed-generator.test.mjs
new file mode 100644
index 0000000..139bc96
--- /dev/null
+++ b/tests/seed-generator.test.mjs
@@ -0,0 +1,23 @@
+import assert from 'node:assert/strict';
+import { generateSeed } from '../src/seed.js';
+
+export const name = 'Seed generator produces eight-digit integers';
+
+export async function run() {
+ const minimum = generateSeed(() => 0);
+ const maximum = generateSeed(() => 0.999999999999);
+ const clampHigh = generateSeed(() => 1.5);
+ const clampLow = generateSeed(() => -0.5);
+ const mid = generateSeed(() => 0.42);
+
+ for (const value of [minimum, maximum, clampHigh, clampLow, mid]) {
+ assert(Number.isInteger(value), `Seed should be an integer (received ${value})`);
+ assert(value >= 10_000_000 && value <= 99_999_999, `Seed ${value} must be eight digits`);
+ assert.equal(String(value).length, 8, `Seed ${value} should contain exactly eight digits`);
+ }
+
+ assert.equal(minimum, 10_000_000);
+ assert.equal(maximum, 99_999_999);
+ assert.equal(clampHigh, 99_999_999);
+ assert.equal(clampLow, 10_000_000);
+}