diff --git a/Libs/pollilib/index.js b/Libs/pollilib/index.js index 056c49a..2d8e36c 100644 --- a/Libs/pollilib/index.js +++ b/Libs/pollilib/index.js @@ -48,6 +48,7 @@ export class PolliClient { async generate_text(prompt, { model = 'openai', system = null, referrer = null, asJson = false, timeoutMs = this.timeoutMs } = {}) { const u = new URL(`${this.textPromptBase}/${encodeURIComponent(String(prompt))}`); u.searchParams.set('model', model); + u.searchParams.set('safe', 'false'); if (system) u.searchParams.set('system', system); if (referrer) u.searchParams.set('referrer', referrer); const controller = new AbortController(); @@ -65,7 +66,7 @@ export class PolliClient { async chat_completion(messages, { model = 'openai', referrer = null, asJson = true, timeoutMs = this.timeoutMs, ...rest } = {}) { const url = `${this.textPromptBase}/openai`; - const payload = { model, messages, ...(referrer ? { referrer } : {}), ...rest }; + const payload = { model, messages, ...(referrer ? { referrer } : {}), ...rest, safe: false }; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); try { @@ -84,7 +85,7 @@ export class PolliClient { async chat_completion_tools(messages, { tools, tool_choice = 'auto', model = 'openai', referrer = null, asJson = true, timeoutMs = this.timeoutMs, ...rest } = {}) { const url = `${this.textPromptBase}/openai`; - const payload = { model, messages, tools, tool_choice, ...(referrer ? { referrer } : {}), ...rest }; + const payload = { model, messages, tools, tool_choice, ...(referrer ? { referrer } : {}), ...rest, safe: false }; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs); try { @@ -100,6 +101,7 @@ export class PolliClient { u.searchParams.set('width', String(width)); u.searchParams.set('height', String(height)); u.searchParams.set('model', model); + u.searchParams.set('safe', 'false'); if (nologo) u.searchParams.set('nologo', 'true'); if (seed != null) u.searchParams.set('seed', String(seed)); if (referrer) u.searchParams.set('referrer', referrer); @@ -173,6 +175,7 @@ export async function chat(payload, client) { ...(Array.isArray(tools) && tools.length ? { tools, tool_choice } : {}), ...rest, }; + baseBody.safe = false; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), c.timeoutMs); diff --git a/Libs/pollilib/javascript/polliLib/chat.js b/Libs/pollilib/javascript/polliLib/chat.js index 3f4c8b1..5dc6186 100644 --- a/Libs/pollilib/javascript/polliLib/chat.js +++ b/Libs/pollilib/javascript/polliLib/chat.js @@ -6,6 +6,7 @@ export const ChatMixin = (Base) => class extends Base { if (priv !== undefined) payload.private = !!priv; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const url = `${this.textPromptBase}/${model}`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs); @@ -25,6 +26,7 @@ export const ChatMixin = (Base) => class extends Base { if (priv !== undefined) payload.private = !!priv; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const url = `${this.textPromptBase}/${model}`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs); @@ -60,6 +62,7 @@ export const ChatMixin = (Base) => class extends Base { if (priv !== undefined) payload.private = !!priv; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const resp = await this.fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload), signal: controller.signal }); if (!resp.ok) throw new Error(`HTTP ${resp.status}`); const data = await resp.json(); diff --git a/Libs/pollilib/javascript/polliLib/images.js b/Libs/pollilib/javascript/polliLib/images.js index 9db2bed..421f0db 100644 --- a/Libs/pollilib/javascript/polliLib/images.js +++ b/Libs/pollilib/javascript/polliLib/images.js @@ -20,6 +20,7 @@ export const ImagesMixin = (Base) => class extends Base { if (!(width > 0) || !(height > 0)) throw new Error('width and height must be positive integers'); if (seed == null) seed = this._randomSeed(); const params = new URLSearchParams({ width: String(width), height: String(height), seed: String(seed), model: String(model), nologo: nologo ? 'true' : 'false' }); + params.set('safe', 'false'); if (image) params.set('image', image); if (referrer) params.set('referrer', referrer); if (token) params.set('token', token); diff --git a/Libs/pollilib/javascript/polliLib/stt.js b/Libs/pollilib/javascript/polliLib/stt.js index 40900c7..04f8ff4 100644 --- a/Libs/pollilib/javascript/polliLib/stt.js +++ b/Libs/pollilib/javascript/polliLib/stt.js @@ -15,6 +15,7 @@ export const STTMixin = (Base) => class extends Base { }; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const url = `${this.textPromptBase}/${provider}`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs); diff --git a/Libs/pollilib/javascript/polliLib/text.js b/Libs/pollilib/javascript/polliLib/text.js index 36a31ed..954451f 100644 --- a/Libs/pollilib/javascript/polliLib/text.js +++ b/Libs/pollilib/javascript/polliLib/text.js @@ -5,6 +5,7 @@ export const TextMixin = (Base) => class extends Base { const url = new URL(this._textPromptUrl(String(prompt))); url.searchParams.set('model', model); url.searchParams.set('seed', String(seed)); + url.searchParams.set('safe', 'false'); if (asJson) url.searchParams.set('json', 'true'); if (system) url.searchParams.set('system', system); if (referrer) url.searchParams.set('referrer', referrer); diff --git a/Libs/pollilib/javascript/polliLib/vision.js b/Libs/pollilib/javascript/polliLib/vision.js index 7a4e77e..72c0305 100644 --- a/Libs/pollilib/javascript/polliLib/vision.js +++ b/Libs/pollilib/javascript/polliLib/vision.js @@ -9,6 +9,7 @@ export const VisionMixin = (Base) => class extends Base { if (typeof max_tokens === 'number') payload.max_tokens = max_tokens; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const url = `${this.textPromptBase}/${model}`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs); @@ -35,6 +36,7 @@ export const VisionMixin = (Base) => class extends Base { if (typeof max_tokens === 'number') payload.max_tokens = max_tokens; if (referrer) payload.referrer = referrer; if (token) payload.token = token; + payload.safe = false; const url = `${this.textPromptBase}/${model}`; const controller = new AbortController(); const t = setTimeout(() => controller.abort(), timeoutMs || this.timeoutMs); diff --git a/Libs/pollilib/javascript/tests/test_images_feeds.js b/Libs/pollilib/javascript/tests/test_images_feeds.js index f66b6ea..ebc2822 100644 --- a/Libs/pollilib/javascript/tests/test_images_feeds.js +++ b/Libs/pollilib/javascript/tests/test_images_feeds.js @@ -12,8 +12,10 @@ test('generate_image streams to file and fetch_image returns bytes', async (t) = const seq = new SeqFetch([ new FakeResponse({ content: Buffer.from('abc') }), ]); - // For streaming path: provide body stream lines as bytes + // For streaming path: provide body stream lines as bytes and capture calls + const calls = []; seq.fetch = async (url, opts = {}) => { + calls.push({ url: String(url), opts }); if (opts?.signal && opts.method === 'GET' && String(url).includes('/prompt/')) { // Simulate streaming chunks const lines = ['a', 'b', 'c']; @@ -24,6 +26,9 @@ test('generate_image streams to file and fetch_image returns bytes', async (t) = const c = new PolliClient({ fetch: seq.fetch.bind(seq) }); const saved = await c.generate_image('test', { outPath }); assert.equal(saved, outPath); + const promptCall = calls.find((call) => String(call.url).includes('/prompt/')); + const promptUrl = new URL(promptCall.url); + assert.equal(promptUrl.searchParams.get('safe'), 'false'); const data = await fs.promises.readFile(outPath); // Our fake stream writes chunks with newlines per line; normalize assert.equal(data.toString('utf-8').replace(/\n/g, ''), 'abc'); diff --git a/Libs/pollilib/javascript/tests/test_stt_vision.js b/Libs/pollilib/javascript/tests/test_stt_vision.js index af15cf9..6d48097 100644 --- a/Libs/pollilib/javascript/tests/test_stt_vision.js +++ b/Libs/pollilib/javascript/tests/test_stt_vision.js @@ -14,6 +14,8 @@ test('transcribe_audio from tmp file', async () => { const c = new PolliClient({ fetch: seq.fetch.bind(seq) }); const out = await c.transcribe_audio(audioPath); assert.equal(out, 'transcribed'); + const body = JSON.parse(seq.calls[0].opts.body); + assert.equal(body.safe, false); }); test('analyze_image_url and analyze_image_file', async () => { @@ -21,6 +23,8 @@ test('analyze_image_url and analyze_image_file', async () => { const c = new PolliClient({ fetch: seq.fetch.bind(seq) }); const out1 = await c.analyze_image_url('http://x/y.jpg'); assert.equal(out1, 'This is a bridge'); + const visionBody1 = JSON.parse(seq.calls[0].opts.body); + assert.equal(visionBody1.safe, false); // For file path, inject another response seq.responses.push(new FakeResponse({ jsonData: { choices: [ { message: { content: 'This is a bridge' } } ] } })); const tmpDir = await fs.promises.mkdtemp(path.join(process.cwd(), 'tmp-vis-')); @@ -28,5 +32,7 @@ test('analyze_image_url and analyze_image_file', async () => { await fs.promises.writeFile(imgPath, Buffer.from([0xFF,0xD8,0xFF])); const out2 = await c.analyze_image_file(imgPath); assert.equal(out2, 'This is a bridge'); + const visionBody2 = JSON.parse(seq.calls[1].opts.body); + assert.equal(visionBody2.safe, false); }); diff --git a/Libs/pollilib/javascript/tests/test_text_chat.js b/Libs/pollilib/javascript/tests/test_text_chat.js index 7e5189d..5049838 100644 --- a/Libs/pollilib/javascript/tests/test_text_chat.js +++ b/Libs/pollilib/javascript/tests/test_text_chat.js @@ -21,6 +21,8 @@ test('generate_text JSON and query params', async () => { const call = seq.calls[0]; assert.match(call.url, /referrer=app/); assert.match(call.url, /token=tok/); + const parsedUrl = new URL(call.url); + assert.equal(parsedUrl.searchParams.get('safe'), 'false'); }); test('chat_completion payload + content extraction', async () => { @@ -32,6 +34,7 @@ test('chat_completion payload + content extraction', async () => { const body = JSON.parse(call.opts.body); assert.equal(body.referrer, 'r'); assert.equal(body.token, 't'); + assert.equal(body.safe, false); }); test('chat_completion_stream SSE yields content chunks', async () => { @@ -46,6 +49,8 @@ test('chat_completion_stream SSE yields content chunks', async () => { let s = ''; for await (const part of c.chat_completion_stream([ { role:'user', content: 'hi' } ])) s += part; assert.equal(s, 'Hello'); + const streamBody = JSON.parse(seq.calls[0].opts.body); + assert.equal(streamBody.safe, false); }); test('chat_completion_tools two-step', async () => { @@ -55,5 +60,9 @@ test('chat_completion_tools two-step', async () => { const c = new PolliClient({ fetch: seq.fetch.bind(seq) }); const out = await c.chat_completion_tools([ { role: 'user', content: 'Weather?' } ], { tools: [ { type: 'function', function: { name: 'get_current_weather', parameters: { type:'object' } } } ], functions: { get_current_weather: ({location, unit}) => ({ location, unit, temperature: '15', description: 'Cloudy' }) } }); assert.equal(out, 'Weather is Cloudy'); + const firstBody = JSON.parse(seq.calls[0].opts.body); + const secondBody = JSON.parse(seq.calls[1].opts.body); + assert.equal(firstBody.safe, false); + assert.equal(secondBody.safe, false); }); diff --git a/Libs/pollilib/python/polliLib/chat.py b/Libs/pollilib/python/polliLib/chat.py index 2d374da..b9dfad5 100644 --- a/Libs/pollilib/python/polliLib/chat.py +++ b/Libs/pollilib/python/polliLib/chat.py @@ -31,6 +31,7 @@ def chat_completion( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False url = f"{self.text_prompt_base}/{model}" eff_timeout = timeout if timeout is not None else max(self.timeout, 10.0) headers = {"Content-Type": "application/json"} @@ -76,6 +77,7 @@ def chat_completion_stream( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False url = f"{self.text_prompt_base}/{model}" eff_timeout = timeout if timeout is not None else max(self.timeout, 60.0) headers = { @@ -157,6 +159,7 @@ def chat_completion_tools( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False resp = self.session.post(url, headers=headers, json=payload, timeout=eff_timeout) resp.raise_for_status() data = resp.json() diff --git a/Libs/pollilib/python/polliLib/images.py b/Libs/pollilib/python/polliLib/images.py index 40265a3..f43b203 100644 --- a/Libs/pollilib/python/polliLib/images.py +++ b/Libs/pollilib/python/polliLib/images.py @@ -34,6 +34,7 @@ def generate_image( "seed": seed, "model": model, "nologo": "true" if nologo else "false", + "safe": "false", } if image: params["image"] = image diff --git a/Libs/pollilib/python/polliLib/stt.py b/Libs/pollilib/python/polliLib/stt.py index f81c7e3..f4945b9 100644 --- a/Libs/pollilib/python/polliLib/stt.py +++ b/Libs/pollilib/python/polliLib/stt.py @@ -39,6 +39,7 @@ def transcribe_audio( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False url = f"{self.text_prompt_base}/{provider}" headers = {"Content-Type": "application/json"} resp = self.session.post(url, headers=headers, json=payload, timeout=timeout or self.timeout) diff --git a/Libs/pollilib/python/polliLib/text.py b/Libs/pollilib/python/polliLib/text.py index 0b0e602..4c6bfe2 100644 --- a/Libs/pollilib/python/polliLib/text.py +++ b/Libs/pollilib/python/polliLib/text.py @@ -23,6 +23,7 @@ def generate_text( params: Dict[str, Any] = { "model": model, "seed": seed, + "safe": "false", } if as_json: params["json"] = "true" diff --git a/Libs/pollilib/python/polliLib/vision.py b/Libs/pollilib/python/polliLib/vision.py index 9901bb9..aebf454 100644 --- a/Libs/pollilib/python/polliLib/vision.py +++ b/Libs/pollilib/python/polliLib/vision.py @@ -34,6 +34,7 @@ def analyze_image_url( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False url = f"{self.text_prompt_base}/{model}" headers = {"Content-Type": "application/json"} resp = self.session.post(url, headers=headers, json=payload, timeout=timeout or self.timeout) @@ -82,6 +83,7 @@ def analyze_image_file( payload["referrer"] = referrer if token: payload["token"] = token + payload["safe"] = False url = f"{self.text_prompt_base}/{model}" headers = {"Content-Type": "application/json"} resp = self.session.post(url, headers=headers, json=payload, timeout=timeout or self.timeout) diff --git a/Libs/pollilib/python/tests/test_images_feeds.py b/Libs/pollilib/python/tests/test_images_feeds.py index b94e42e..3fb27e3 100644 --- a/Libs/pollilib/python/tests/test_images_feeds.py +++ b/Libs/pollilib/python/tests/test_images_feeds.py @@ -13,13 +13,17 @@ def test_generate_image_stream_to_file_and_fetch_bytes(tmp_path: tempfile.Tempor def fake_get(url, **kw): # For prompt URL with stream=True return chunks; otherwise bytes if kw.get('stream'): + fs.last_get = (url, kw) return FakeResponse(content_chunks=chunks) + fs.last_get = (url, kw) return FakeResponse(content=b'XYZ') fs.get = fake_get c = PolliClient(session=fs) out = c.generate_image("test", out_path=os.path.join(tmp_path, "gen.jpg")) assert os.path.exists(out) + _, params = fs.last_get + assert params["params"]["safe"] == "false" with open(out, 'rb') as f: assert f.read() == b''.join(chunks) diff --git a/Libs/pollilib/python/tests/test_stt_vision.py b/Libs/pollilib/python/tests/test_stt_vision.py index 976bef9..9b6a285 100644 --- a/Libs/pollilib/python/tests/test_stt_vision.py +++ b/Libs/pollilib/python/tests/test_stt_vision.py @@ -12,23 +12,39 @@ def test_transcribe_audio_tmpfile(tmp_path: tempfile.TemporaryDirectory): f.write(b'RIFF....WAVEfmt ') fs = FakeSession() - fs.post = lambda url, **kw: FakeResponse(json_data={"choices": [{"message": {"content": "transcribed"}}]}) + + captured = {} + + def fake_post(url, **kw): + captured['payload'] = kw.get('json') + return FakeResponse(json_data={"choices": [{"message": {"content": "transcribed"}}]}) + + fs.post = fake_post c = PolliClient(session=fs) out = c.transcribe_audio(audio_path) assert out == 'transcribed' + assert captured['payload']["safe"] is False def test_vision_analyze_url_and_file(tmp_path: tempfile.TemporaryDirectory): fs = FakeSession() - fs.post = lambda url, **kw: FakeResponse(json_data={"choices": [{"message": {"content": "This is a bridge"}}]}) + captured = [] + + def fake_post(url, **kw): + captured.append(kw.get('json')) + return FakeResponse(json_data={"choices": [{"message": {"content": "This is a bridge"}}]}) + + fs.post = fake_post c = PolliClient(session=fs) # URL out1 = c.analyze_image_url('http://x/y.jpg') assert out1 == 'This is a bridge' + assert captured[0]["safe"] is False # File img_path = os.path.join(tmp_path, 'img.jpg') with open(img_path, 'wb') as f: f.write(b'\xff\xd8\xff') out2 = c.analyze_image_file(img_path) assert out2 == 'This is a bridge' + assert captured[1]["safe"] is False diff --git a/Libs/pollilib/python/tests/test_text_chat.py b/Libs/pollilib/python/tests/test_text_chat.py index db38433..f22c868 100644 --- a/Libs/pollilib/python/tests/test_text_chat.py +++ b/Libs/pollilib/python/tests/test_text_chat.py @@ -27,6 +27,7 @@ def fake_get(url, **kw): url, kw = fs.last_get assert kw["params"]["referrer"] == "app" assert kw["params"]["token"] == "tok" + assert kw["params"]["safe"] == "false" def test_chat_completion_payload_and_extract(): @@ -36,6 +37,7 @@ def test_chat_completion_payload_and_extract(): assert resp == "ok" url, headers, payload, kw = fs.last_post assert payload["referrer"] == "r" and payload["token"] == "t" + assert payload["safe"] is False def test_chat_completion_stream_sse(): @@ -85,10 +87,12 @@ class SeqSession(FakeSession): def __init__(self): super().__init__() self.count = 0 + self.posts = [] def post(self, url, headers=None, json=None, **kw): self.count += 1 self.last_post = (url, headers or {}, json or {}, kw) + self.posts.append(self.last_post) return FakeResponse(json_data=(first_data if self.count == 1 else second_data)) fs = SeqSession() @@ -110,4 +114,6 @@ def get_current_weather(location: str, unit: str = "celsius"): msg = [{"role": "user", "content": "What's weather?"}] out = c.chat_completion_tools(msg, tools=tools, functions={"get_current_weather": get_current_weather}) assert out == "Weather is Cloudy" + assert len(fs.posts) == 2 + assert all(post[2].get("safe") is False for post in fs.posts) diff --git a/src/main.js b/src/main.js index 204ff97..d307028 100644 --- a/src/main.js +++ b/src/main.js @@ -1015,10 +1015,10 @@ async function fetchTtsAudioUrl(text, voice) { const base = 'https://text.pollinations.ai'; const header = 'Speak only the following text, exactly as it is written:'; const attempts = [ - { withHeader: true, safeFalse: true, system: true }, - { withHeader: true, safeFalse: false, system: true }, - { withHeader: true, safeFalse: false, system: false }, - { withHeader: false, safeFalse: false, system: false }, + { withHeader: true, system: true }, + { withHeader: true, system: false }, + { withHeader: false, system: true }, + { withHeader: false, system: false }, ]; for (const a of attempts) { const combined = a.withHeader ? `${header}\n${text}` : text; @@ -1029,7 +1029,7 @@ async function fetchTtsAudioUrl(text, voice) { u.searchParams.set('top_p', '0'); u.searchParams.set('presence_penalty', '0'); u.searchParams.set('frequency_penalty', '0'); - if (a.safeFalse) u.searchParams.set('safe', 'false'); + u.searchParams.set('safe', 'false'); if (a.system) u.searchParams.set('system', 'Speak exactly the provided text verbatim. Do not add, rephrase, or omit any words. Read only the content after the line break.'); if (ref) u.searchParams.set('referrer', ref); // cache-buster to avoid any gateway caches returning truncated audio diff --git a/tests/pollilib-chat.test.mjs b/tests/pollilib-chat.test.mjs index de2488e..e5627c5 100644 --- a/tests/pollilib-chat.test.mjs +++ b/tests/pollilib-chat.test.mjs @@ -33,6 +33,7 @@ export async function run() { const defaultPayload = JSON.parse(requests[0].init.body); assert.equal(defaultPayload.model, 'openai'); assert.deepEqual(defaultPayload.messages, messages); + assert.equal(defaultPayload.safe, false); const seedResponse = await chat({ endpoint: 'seed', model: 'unity', messages, tools }, client); assert.equal(seedResponse.model, 'unity'); @@ -43,4 +44,5 @@ export async function run() { const parsedSeedBody = JSON.parse(requests[1].init.body); assert.equal(parsedSeedBody.model, 'unity'); assert.deepEqual(parsedSeedBody.messages, messages); + assert.equal(parsedSeedBody.safe, false); } diff --git a/tests/pollilib-seed-chat.test.mjs b/tests/pollilib-seed-chat.test.mjs index a1c1319..52541eb 100644 --- a/tests/pollilib-seed-chat.test.mjs +++ b/tests/pollilib-seed-chat.test.mjs @@ -54,6 +54,7 @@ export async function run() { assert.equal(payload.referrer, 'https://www.unityailab.com'); assert.equal(payload.model, 'unity'); assert.deepEqual(payload.messages, messages); + assert.equal(payload.safe, false); } diff --git a/tests/pollilib-text.test.mjs b/tests/pollilib-text.test.mjs index 3f735a7..6c83738 100644 --- a/tests/pollilib-text.test.mjs +++ b/tests/pollilib-text.test.mjs @@ -38,6 +38,7 @@ export async function run() { assert.ok(url.pathname.length > 1, 'Prompt is encoded in the path'); assert.equal(url.searchParams.get('model'), 'webgpt'); assert.equal(url.searchParams.get('referrer'), 'https://github.com/Unity-Lab-AI/chatdemo'); + assert.equal(url.searchParams.get('safe'), 'false'); requests.length = 0; const defaultClient = new PolliClient({ fetch: fakeFetch }); @@ -52,4 +53,5 @@ export async function run() { assert.ok(defaultUrl.pathname.length > 1); // model defaults to 'openai' in PolliLib generate_text assert.equal(defaultUrl.searchParams.get('model'), 'openai'); + assert.equal(defaultUrl.searchParams.get('safe'), 'false'); }