PollinationsAI.py 20 KB

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