Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Libs/pollilib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions Libs/pollilib/javascript/polliLib/chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/javascript/polliLib/images.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/javascript/polliLib/stt.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/javascript/polliLib/text.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions Libs/pollilib/javascript/polliLib/vision.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
7 changes: 6 additions & 1 deletion Libs/pollilib/javascript/tests/test_images_feeds.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'];
Expand All @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions Libs/pollilib/javascript/tests/test_stt_vision.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,25 @@ 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 () => {
const seq = new SeqFetch([ new FakeResponse({ jsonData: { choices: [ { message: { content: 'This is a bridge' } } ] } }) ]);
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-'));
const imgPath = path.join(tmpDir, 'i.jpg');
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);
});

9 changes: 9 additions & 0 deletions Libs/pollilib/javascript/tests/test_text_chat.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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 () => {
Expand All @@ -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);
});

3 changes: 3 additions & 0 deletions Libs/pollilib/python/polliLib/chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down Expand Up @@ -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 = {
Expand Down Expand Up @@ -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()
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/python/polliLib/images.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def generate_image(
"seed": seed,
"model": model,
"nologo": "true" if nologo else "false",
"safe": "false",
}
if image:
params["image"] = image
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/python/polliLib/stt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions Libs/pollilib/python/polliLib/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def generate_text(
params: Dict[str, Any] = {
"model": model,
"seed": seed,
"safe": "false",
}
if as_json:
params["json"] = "true"
Expand Down
2 changes: 2 additions & 0 deletions Libs/pollilib/python/polliLib/vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Libs/pollilib/python/tests/test_images_feeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
20 changes: 18 additions & 2 deletions Libs/pollilib/python/tests/test_stt_vision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

6 changes: 6 additions & 0 deletions Libs/pollilib/python/tests/test_text_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand All @@ -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():
Expand Down Expand Up @@ -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()
Expand All @@ -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)

10 changes: 5 additions & 5 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/pollilib-chat.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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);
}
1 change: 1 addition & 0 deletions tests/pollilib-seed-chat.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}


Expand Down
Loading