PollinationsAI.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. from __future__ import annotations
  2. import time
  3. import json
  4. import random
  5. import requests
  6. import asyncio
  7. from urllib.parse import quote_plus
  8. from typing import Optional
  9. from aiohttp import ClientSession
  10. from .helper import filter_none, format_image_prompt
  11. from .base_provider import AsyncGeneratorProvider, ProviderModelMixin
  12. from ..typing import AsyncResult, Messages, MediaListType
  13. from ..image import is_data_an_audio
  14. from ..errors import ModelNotFoundError, ResponseError
  15. from ..requests import see_stream
  16. from ..requests.raise_for_status import raise_for_status
  17. from ..requests.aiohttp import get_connector
  18. from ..image.copy_images import save_response_media
  19. from ..image import use_aspect_ratio
  20. from ..providers.response import FinishReason, Usage, ToolCalls, ImageResponse, Reasoning, TitleGeneration, SuggestedFollowups
  21. from ..tools.media import render_messages
  22. from ..constants import STATIC_URL
  23. from .. import debug
  24. DEFAULT_HEADERS = {
  25. "accept": "*/*",
  26. 'accept-language': 'en-US,en;q=0.9',
  27. "user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
  28. "referer": "https://pollinations.ai/",
  29. "origin": "https://pollinations.ai",
  30. }
  31. FOLLOWUPS_TOOLS = [{
  32. "type": "function",
  33. "function": {
  34. "name": "options",
  35. "description": "Provides options for the conversation",
  36. "parameters": {
  37. "properties": {
  38. "title": {
  39. "title": "Conversation Title",
  40. "type": "string"
  41. },
  42. "followups": {
  43. "items": {
  44. "type": "string"
  45. },
  46. "title": "Suggested Followups",
  47. "type": "array"
  48. }
  49. },
  50. "title": "Conversation",
  51. "type": "object"
  52. }
  53. }
  54. }]
  55. FOLLOWUPS_DEVELOPER_MESSAGE = [{
  56. "role": "developer",
  57. "content": "Prefix conversation title with one or more emojies. Suggested 4 Followups"
  58. }]
  59. class PollinationsAI(AsyncGeneratorProvider, ProviderModelMixin):
  60. label = "Pollinations AI"
  61. url = "https://pollinations.ai"
  62. login_url = "https://auth.pollinations.ai"
  63. working = True
  64. supports_system_message = True
  65. supports_message_history = True
  66. # API endpoints
  67. text_api_endpoint = "https://text.pollinations.ai"
  68. openai_endpoint = "https://text.pollinations.ai/openai"
  69. image_api_endpoint = "https://image.pollinations.ai/"
  70. # Models configuration
  71. default_model = "openai"
  72. default_image_model = "flux"
  73. default_vision_model = default_model
  74. default_audio_model = "openai-audio"
  75. text_models = [default_model, "evil"]
  76. image_models = [default_image_model]
  77. audio_models = {default_audio_model: []}
  78. extra_image_models = ["flux-pro", "flux-dev", "flux-schnell", "midjourney", "dall-e-3", "turbo"]
  79. vision_models = [default_vision_model, "gpt-4o-mini", "openai", "openai-large", "openai-reasoning", "searchgpt"]
  80. _models_loaded = False
  81. # https://github.com/pollinations/pollinations/blob/master/text.pollinations.ai/generateTextPortkey.js#L15
  82. model_aliases = {
  83. ### Text Models ###
  84. "gpt-4o-mini": "openai",
  85. "gpt-4.1-nano": "openai-fast",
  86. "gpt-4": "openai-large",
  87. "gpt-4o": "openai-large",
  88. "gpt-4.1": "openai-large",
  89. "gpt-4o-audio": "openai-audio",
  90. "o4-mini": "openai-reasoning",
  91. "gpt-4.1-mini": "openai",
  92. "command-r-plus": "command-r",
  93. "gemini-2.5-flash": "gemini",
  94. "gemini-2.0-flash-thinking": "gemini-thinking",
  95. "qwen-2.5-coder-32b": "qwen-coder",
  96. "llama-3.3-70b": "llama",
  97. "llama-4-scout": "llamascout",
  98. "llama-4-scout-17b": "llamascout",
  99. "mistral-small-3.1-24b": "mistral",
  100. "deepseek-r1": "deepseek-reasoning-large",
  101. "deepseek-r1-distill-llama-70b": "deepseek-reasoning-large",
  102. #"deepseek-r1-distill-llama-70b": "deepseek-r1-llama",
  103. #"mistral-small-3.1-24b": "unity", # Personas
  104. #"mirexa": "mirexa", # Personas
  105. #"midijourney": "midijourney", # Personas
  106. #"rtist": "rtist", # Personas
  107. #"searchgpt": "searchgpt",
  108. #"evil": "evil", # Personas
  109. "deepseek-r1-distill-qwen-32b": "deepseek-reasoning",
  110. "phi-4": "phi",
  111. #"pixtral-12b": "pixtral",
  112. #"hormoz-8b": "hormoz",
  113. "qwq-32b": "qwen-qwq",
  114. #"hypnosis-tracy-7b": "hypnosis-tracy", # Personas
  115. #"mistral-?": "sur", # Personas
  116. "deepseek-v3": "deepseek",
  117. "deepseek-v3-0324": "deepseek",
  118. #"bidara": "bidara", # Personas
  119. "grok-3-mini": "grok",
  120. ### Audio Models ###
  121. "gpt-4o-mini-audio": "openai-audio",
  122. ### Image Models ###
  123. "sdxl-turbo": "turbo",
  124. "gpt-image": "gptimage",
  125. }
  126. @classmethod
  127. def get_model(cls, model: str) -> str:
  128. """Get the internal model name from the user-provided model name."""
  129. if not model:
  130. return cls.default_model
  131. # Check if the model exists directly in our model lists
  132. if model in cls.text_models or model in cls.image_models or model in cls.audio_models:
  133. return model
  134. # Check if there's an alias for this model
  135. if model in cls.model_aliases:
  136. return cls.model_aliases[model]
  137. # If no match is found, raise an error
  138. raise ModelNotFoundError(f"Model {model} not found")
  139. @classmethod
  140. def get_models(cls, **kwargs):
  141. if not cls._models_loaded:
  142. try:
  143. # Update of image models
  144. image_response = requests.get("https://image.pollinations.ai/models")
  145. if image_response.ok:
  146. new_image_models = image_response.json()
  147. else:
  148. new_image_models = []
  149. # Combine image models without duplicates
  150. all_image_models = [cls.default_image_model] # Start with default model
  151. # Add extra image models if not already in the list
  152. for model in cls.extra_image_models + new_image_models:
  153. if model not in all_image_models:
  154. all_image_models.append(model)
  155. cls.image_models = all_image_models
  156. text_response = requests.get("https://text.pollinations.ai/models")
  157. text_response.raise_for_status()
  158. models = text_response.json()
  159. # Purpose of audio models
  160. cls.audio_models = {
  161. model.get("name"): model.get("voices")
  162. for model in models
  163. if "output_modalities" in model and "audio" in model["output_modalities"] and model.get("name") != "gemini"
  164. }
  165. cls.vision_models.extend([
  166. model.get("name")
  167. for model in models
  168. if model.get("vision") and model not in cls.vision_models
  169. ])
  170. for alias, model in cls.model_aliases.items():
  171. if model in cls.vision_models and alias not in cls.vision_models:
  172. cls.vision_models.append(alias)
  173. # Create a set of unique text models starting with default model
  174. unique_text_models = cls.text_models.copy()
  175. # Add models from vision_models
  176. unique_text_models.extend(cls.vision_models)
  177. # Add models from the API response
  178. for model in models:
  179. model_name = model.get("name")
  180. if model_name and "input_modalities" in model and "text" in model["input_modalities"]:
  181. unique_text_models.append(model_name)
  182. # Convert to list and update text_models
  183. cls.text_models = list(dict.fromkeys(unique_text_models))
  184. cls._models_loaded = True
  185. except Exception as e:
  186. # Save default models in case of an error
  187. if not cls.text_models:
  188. cls.text_models = [cls.default_model]
  189. if not cls.image_models:
  190. cls.image_models = [cls.default_image_model]
  191. debug.error(f"Failed to fetch models: {e}")
  192. # Return unique models across all categories
  193. all_models = cls.text_models.copy()
  194. all_models.extend(cls.image_models)
  195. all_models.extend(cls.audio_models.keys())
  196. if cls.default_audio_model in cls.audio_models:
  197. all_models.extend(cls.audio_models[cls.default_audio_model])
  198. return list(dict.fromkeys(all_models))
  199. @classmethod
  200. def get_grouped_models(cls) -> dict[str, list[str]]:
  201. cls.get_models()
  202. return [
  203. {"group": "Text Generation", "models": cls.text_models},
  204. {"group": "Image Generation", "models": cls.image_models},
  205. {"group": "Audio Generation", "models": list(cls.audio_models.keys())},
  206. {"group": "Audio Voices", "models": cls.audio_models[cls.default_audio_model]}
  207. ]
  208. @classmethod
  209. async def create_async_generator(
  210. cls,
  211. model: str,
  212. messages: Messages,
  213. stream: bool = True,
  214. proxy: str = None,
  215. cache: bool = False,
  216. referrer: str = STATIC_URL,
  217. api_key: str = None,
  218. extra_body: dict = {},
  219. # Image generation parameters
  220. prompt: str = None,
  221. aspect_ratio: str = "1:1",
  222. width: int = None,
  223. height: int = None,
  224. seed: Optional[int] = None,
  225. nologo: bool = True,
  226. private: bool = False,
  227. enhance: bool = False,
  228. safe: bool = False,
  229. n: int = 1,
  230. # Text generation parameters
  231. media: MediaListType = None,
  232. temperature: float = None,
  233. presence_penalty: float = None,
  234. top_p: float = None,
  235. frequency_penalty: float = None,
  236. response_format: Optional[dict] = None,
  237. extra_parameters: list[str] = ["tools", "parallel_tool_calls", "tool_choice", "reasoning_effort", "logit_bias", "voice", "modalities", "audio"],
  238. **kwargs
  239. ) -> AsyncResult:
  240. # Load model list
  241. cls.get_models()
  242. if not model:
  243. has_audio = "audio" in kwargs or "audio" in kwargs.get("modalities", [])
  244. if not has_audio and media is not None:
  245. for media_data, filename in media:
  246. if is_data_an_audio(media_data, filename):
  247. has_audio = True
  248. break
  249. model = cls.default_audio_model if has_audio else model
  250. try:
  251. model = cls.get_model(model)
  252. except ModelNotFoundError:
  253. pass
  254. if model in cls.image_models:
  255. async for chunk in cls._generate_image(
  256. model=model,
  257. prompt=format_image_prompt(messages, prompt),
  258. proxy=proxy,
  259. aspect_ratio=aspect_ratio,
  260. width=width,
  261. height=height,
  262. seed=seed,
  263. cache=cache,
  264. nologo=nologo,
  265. private=private,
  266. enhance=enhance,
  267. safe=safe,
  268. n=n,
  269. referrer=referrer,
  270. api_key=api_key
  271. ):
  272. yield chunk
  273. else:
  274. if prompt is not None and len(messages) == 1:
  275. messages = [{
  276. "role": "user",
  277. "content": prompt
  278. }]
  279. if model and model in cls.audio_models[cls.default_audio_model]:
  280. kwargs["audio"] = {
  281. "voice": model,
  282. }
  283. model = cls.default_audio_model
  284. async for result in cls._generate_text(
  285. model=model,
  286. messages=messages,
  287. media=media,
  288. proxy=proxy,
  289. temperature=temperature,
  290. presence_penalty=presence_penalty,
  291. top_p=top_p,
  292. frequency_penalty=frequency_penalty,
  293. response_format=response_format,
  294. seed=seed,
  295. cache=cache,
  296. stream=stream,
  297. extra_parameters=extra_parameters,
  298. referrer=referrer,
  299. api_key=api_key,
  300. extra_body=extra_body,
  301. **kwargs
  302. ):
  303. yield result
  304. @classmethod
  305. async def _generate_image(
  306. cls,
  307. model: str,
  308. prompt: str,
  309. proxy: str,
  310. aspect_ratio: str,
  311. width: int,
  312. height: int,
  313. seed: Optional[int],
  314. cache: bool,
  315. nologo: bool,
  316. private: bool,
  317. enhance: bool,
  318. safe: bool,
  319. n: int,
  320. referrer: str,
  321. api_key: str
  322. ) -> AsyncResult:
  323. params = use_aspect_ratio({
  324. "width": width,
  325. "height": height,
  326. "model": model,
  327. "nologo": str(nologo).lower(),
  328. "private": str(private).lower(),
  329. "enhance": str(enhance).lower(),
  330. "safe": str(safe).lower(),
  331. }, aspect_ratio)
  332. query = "&".join(f"{k}={quote_plus(str(v))}" for k, v in params.items() if v is not None)
  333. prompt = quote_plus(prompt)[:2048-len(cls.image_api_endpoint)-len(query)-8]
  334. url = f"{cls.image_api_endpoint}prompt/{prompt}?{query}"
  335. def get_image_url(i: int, seed: Optional[int] = None):
  336. if i == 0:
  337. if not cache and seed is None:
  338. seed = random.randint(0, 2**32)
  339. else:
  340. seed = random.randint(0, 2**32)
  341. return f"{url}&seed={seed}" if seed else url
  342. headers = {"referer": referrer}
  343. if api_key:
  344. headers["Authorization"] = f"Bearer {api_key}"
  345. async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
  346. responses = set()
  347. responses.add(Reasoning(status=f"Generating {n} {'image' if n == 1 else 'images'}..."))
  348. finished = 0
  349. start = time.time()
  350. async def get_image(responses: set, i: int, seed: Optional[int] = None):
  351. nonlocal finished
  352. async with session.get(get_image_url(i, seed), allow_redirects=False, headers=headers) as response:
  353. try:
  354. await raise_for_status(response)
  355. except Exception as e:
  356. debug.error(f"Error fetching image: {e}")
  357. responses.add(ImageResponse(str(response.url), prompt))
  358. finished += 1
  359. responses.add(Reasoning(status=f"Image {finished}/{n} generated in {time.time() - start:.2f}s"))
  360. tasks = []
  361. for i in range(int(n)):
  362. tasks.append(asyncio.create_task(get_image(responses, i, seed)))
  363. while finished < n or len(responses) > 0:
  364. while len(responses) > 0:
  365. yield responses.pop()
  366. await asyncio.sleep(0.1)
  367. await asyncio.gather(*tasks)
  368. @classmethod
  369. async def _generate_text(
  370. cls,
  371. model: str,
  372. messages: Messages,
  373. media: MediaListType,
  374. proxy: str,
  375. temperature: float,
  376. presence_penalty: float,
  377. top_p: float,
  378. frequency_penalty: float,
  379. response_format: Optional[dict],
  380. seed: Optional[int],
  381. cache: bool,
  382. stream: bool,
  383. extra_parameters: list[str],
  384. referrer: str,
  385. api_key: str,
  386. extra_body: dict,
  387. **kwargs
  388. ) -> AsyncResult:
  389. if not cache and seed is None:
  390. seed = random.randint(0, 2**32)
  391. async with ClientSession(headers=DEFAULT_HEADERS, connector=get_connector(proxy=proxy)) as session:
  392. if model in cls.audio_models:
  393. if "audio" in kwargs and kwargs.get("audio", {}).get("voice") is None:
  394. kwargs["audio"]["voice"] = cls.audio_models[model][0]
  395. url = cls.text_api_endpoint
  396. stream = False
  397. else:
  398. url = cls.openai_endpoint
  399. extra_body.update({param: kwargs[param] for param in extra_parameters if param in kwargs})
  400. data = filter_none(
  401. messages=list(render_messages(messages, media)),
  402. model=model,
  403. temperature=temperature,
  404. presence_penalty=presence_penalty,
  405. top_p=top_p,
  406. frequency_penalty=frequency_penalty,
  407. response_format=response_format,
  408. stream=stream,
  409. seed=seed,
  410. cache=cache,
  411. **extra_body
  412. )
  413. headers = {"referer": referrer}
  414. if api_key:
  415. headers["Authorization"] = f"Bearer {api_key}"
  416. async with session.post(url, json=data, headers=headers) as response:
  417. if response.status == 400:
  418. debug.error(f"Error: 400 - Bad Request: {data}")
  419. await raise_for_status(response)
  420. if response.headers["content-type"].startswith("text/plain"):
  421. yield await response.text()
  422. return
  423. elif response.headers["content-type"].startswith("text/event-stream"):
  424. reasoning = False
  425. async for result in see_stream(response.content):
  426. if "error" in result:
  427. raise ResponseError(result["error"].get("message", result["error"]))
  428. if result.get("usage") is not None:
  429. yield Usage(**result["usage"])
  430. choices = result.get("choices", [{}])
  431. choice = choices.pop() if choices else {}
  432. content = choice.get("delta", {}).get("content")
  433. if content:
  434. yield content
  435. tool_calls = choice.get("delta", {}).get("tool_calls")
  436. if tool_calls:
  437. yield ToolCalls(choice["delta"]["tool_calls"])
  438. reasoning_content = choice.get("delta", {}).get("reasoning_content")
  439. if reasoning_content:
  440. reasoning = True
  441. yield Reasoning(reasoning_content)
  442. finish_reason = choice.get("finish_reason")
  443. if finish_reason:
  444. yield FinishReason(finish_reason)
  445. if reasoning:
  446. yield Reasoning(status="Done")
  447. if kwargs.get("action") == "next":
  448. data = {
  449. "model": "openai",
  450. "messages": messages + FOLLOWUPS_DEVELOPER_MESSAGE,
  451. "tool_choice": "required",
  452. "tools": FOLLOWUPS_TOOLS
  453. }
  454. async with session.post(url, json=data, headers=headers) as response:
  455. try:
  456. await raise_for_status(response)
  457. tool_calls = (await response.json()).get("choices", [{}])[0].get("message", {}).get("tool_calls", [])
  458. if tool_calls:
  459. arguments = json.loads(tool_calls.pop().get("function", {}).get("arguments"))
  460. if arguments.get("title"):
  461. yield TitleGeneration(arguments.get("title"))
  462. if arguments.get("followups"):
  463. yield SuggestedFollowups(arguments.get("followups"))
  464. except Exception as e:
  465. debug.error("Error generating title and followups")
  466. debug.error(e)
  467. elif response.headers["content-type"].startswith("application/json"):
  468. result = await response.json()
  469. if "choices" in result:
  470. choice = result["choices"][0]
  471. message = choice.get("message", {})
  472. content = message.get("content", "")
  473. if content:
  474. yield content
  475. if "tool_calls" in message:
  476. yield ToolCalls(message["tool_calls"])
  477. else:
  478. raise ResponseError(result)
  479. if result.get("usage") is not None:
  480. yield Usage(**result["usage"])
  481. finish_reason = choice.get("finish_reason")
  482. if finish_reason:
  483. yield FinishReason(finish_reason)
  484. else:
  485. async for chunk in save_response_media(response, format_image_prompt(messages), [model, extra_body.get("audio", {}).get("voice")]):
  486. yield chunk
  487. return