Gemini.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493
  1. from __future__ import annotations
  2. import os
  3. import json
  4. import random
  5. import re
  6. import base64
  7. import asyncio
  8. import time
  9. from urllib.parse import quote_plus, unquote_plus
  10. from pathlib import Path
  11. from aiohttp import ClientSession, BaseConnector
  12. try:
  13. import nodriver
  14. has_nodriver = True
  15. except ImportError:
  16. has_nodriver = False
  17. from ... import debug
  18. from ...typing import Messages, Cookies, MediaListType, AsyncResult, AsyncIterator
  19. from ...providers.response import JsonConversation, Reasoning, RequestLogin, ImageResponse, YouTube, AudioResponse
  20. from ...requests.raise_for_status import raise_for_status
  21. from ...requests.aiohttp import get_connector
  22. from ...requests import get_nodriver
  23. from ...image.copy_images import get_filename, get_media_dir, ensure_media_dir
  24. from ...errors import MissingAuthError
  25. from ...image import to_bytes
  26. from ...cookies import get_cookies_dir
  27. from ...tools.media import merge_media
  28. from ..base_provider import AsyncGeneratorProvider, ProviderModelMixin
  29. from ..helper import format_prompt, get_cookies, get_last_user_message, format_image_prompt
  30. from ... import debug
  31. REQUEST_HEADERS = {
  32. "authority": "gemini.google.com",
  33. "origin": "https://gemini.google.com",
  34. "referer": "https://gemini.google.com/",
  35. 'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.0.0 Safari/537.36',
  36. 'x-same-domain': '1',
  37. }
  38. REQUEST_BL_PARAM = "boq_assistant-bard-web-server_20240519.16_p0"
  39. REQUEST_URL = "https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate"
  40. UPLOAD_IMAGE_URL = "https://content-push.googleapis.com/upload/"
  41. UPLOAD_IMAGE_HEADERS = {
  42. "authority": "content-push.googleapis.com",
  43. "accept": "*/*",
  44. "accept-language": "en-US,en;q=0.7",
  45. "authorization": "Basic c2F2ZXM6cyNMdGhlNmxzd2F2b0RsN3J1d1U=",
  46. "content-type": "application/x-www-form-urlencoded;charset=UTF-8",
  47. "origin": "https://gemini.google.com",
  48. "push-id": "feeds/mcudyrk2a4khkz",
  49. "referer": "https://gemini.google.com/",
  50. "x-goog-upload-command": "start",
  51. "x-goog-upload-header-content-length": "",
  52. "x-goog-upload-protocol": "resumable",
  53. "x-tenant-id": "bard-storage",
  54. }
  55. GOOGLE_COOKIE_DOMAIN = ".google.com"
  56. ROTATE_COOKIES_URL = "https://accounts.google.com/RotateCookies"
  57. GGOGLE_SID_COOKIE = "__Secure-1PSID"
  58. models = {
  59. "gemini-2.5-pro-exp": {"x-goog-ext-525001261-jspb": '[1,null,null,null,"2525e3954d185b3c"]'},
  60. "gemini-2.5-flash": {"x-goog-ext-525001261-jspb": '[1,null,null,null,"35609594dbe934d8"]'},
  61. "gemini-2.0-flash-thinking-exp": {"x-goog-ext-525001261-jspb": '[1,null,null,null,"7ca48d02d802f20a"]'},
  62. "gemini-deep-research": {"x-goog-ext-525001261-jspb": '[1,null,null,null,"cd472a54d2abba7e"]'},
  63. "gemini-2.0-flash": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
  64. "gemini-2.0-flash-exp": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f299729663a2343f"]'},
  65. "gemini-2.0-flash-thinking": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"9c17b1863f581b8a"]'},
  66. "gemini-2.0-flash-thinking-with-apps": {"x-goog-ext-525001261-jspb": '[null,null,null,null,"f8f8f5ea629f5d37"]'},
  67. "gemini-audio": {}
  68. }
  69. class Gemini(AsyncGeneratorProvider, ProviderModelMixin):
  70. label = "Google Gemini"
  71. url = "https://gemini.google.com"
  72. needs_auth = True
  73. working = True
  74. use_nodriver = True
  75. default_model = ""
  76. default_image_model = default_model
  77. default_vision_model = default_model
  78. image_models = [default_image_model]
  79. models = [
  80. default_model, *models.keys()
  81. ]
  82. model_aliases = {
  83. "gemini-2.0": "",
  84. "gemini-2.5-pro": "gemini-2.5-pro-exp"
  85. }
  86. synthesize_content_type = "audio/vnd.wav"
  87. _cookies: Cookies = None
  88. _snlm0e: str = None
  89. _sid: str = None
  90. auto_refresh = True
  91. refresh_interval = 540
  92. rotate_tasks = {}
  93. @classmethod
  94. async def nodriver_login(cls, proxy: str = None) -> AsyncIterator[str]:
  95. if not has_nodriver:
  96. debug.log("Skip nodriver login in Gemini provider")
  97. return
  98. browser, stop_browser = await get_nodriver(proxy=proxy, user_data_dir="gemini")
  99. try:
  100. yield RequestLogin(cls.label, os.environ.get("G4F_LOGIN_URL", ""))
  101. page = await browser.get(f"{cls.url}/app")
  102. await page.select("div.ql-editor.textarea", 240)
  103. cookies = {}
  104. for c in await page.send(nodriver.cdp.network.get_cookies([cls.url])):
  105. cookies[c.name] = c.value
  106. await page.close()
  107. cls._cookies = cookies
  108. finally:
  109. stop_browser()
  110. @classmethod
  111. async def start_auto_refresh(cls, proxy: str = None) -> None:
  112. """
  113. Start the background task to automatically refresh cookies.
  114. """
  115. while True:
  116. try:
  117. new_1psidts = await rotate_1psidts(cls.url, cls._cookies, proxy)
  118. except Exception as e:
  119. debug.error(f"Failed to refresh cookies: {e}")
  120. task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
  121. if task:
  122. task.cancel()
  123. debug.error(
  124. "Failed to refresh cookies. Background auto refresh task canceled."
  125. )
  126. debug.log(f"Gemini: Cookies refreshed. New __Secure-1PSIDTS: {new_1psidts}")
  127. if new_1psidts:
  128. cls._cookies["__Secure-1PSIDTS"] = new_1psidts
  129. await asyncio.sleep(cls.refresh_interval)
  130. @classmethod
  131. async def create_async_generator(
  132. cls,
  133. model: str,
  134. messages: Messages,
  135. proxy: str = None,
  136. cookies: Cookies = None,
  137. connector: BaseConnector = None,
  138. media: MediaListType = None,
  139. return_conversation: bool = True,
  140. conversation: Conversation = None,
  141. language: str = "en",
  142. prompt: str = None,
  143. audio: dict = None,
  144. **kwargs
  145. ) -> AsyncResult:
  146. if model in cls.model_aliases:
  147. model = cls.model_aliases[model]
  148. if audio is not None or model == "gemini-audio":
  149. prompt = format_image_prompt(messages, prompt)
  150. filename = get_filename(["gemini"], prompt, ".ogx", prompt)
  151. ensure_media_dir()
  152. path = os.path.join(get_media_dir(), filename)
  153. with open(path, "wb") as f:
  154. async for chunk in cls.synthesize({"text": prompt}, proxy):
  155. f.write(chunk)
  156. yield AudioResponse(f"/media/{filename}", text=prompt)
  157. return
  158. cls._cookies = cookies or cls._cookies or get_cookies(GOOGLE_COOKIE_DOMAIN, False, True)
  159. if conversation is not None and getattr(conversation, "model", None) != model:
  160. conversation = None
  161. prompt = format_prompt(messages) if conversation is None else get_last_user_message(messages)
  162. base_connector = get_connector(connector, proxy)
  163. async with ClientSession(
  164. headers=REQUEST_HEADERS,
  165. connector=base_connector
  166. ) as session:
  167. if not cls._snlm0e:
  168. await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
  169. if not cls._snlm0e:
  170. try:
  171. async for chunk in cls.nodriver_login(proxy):
  172. yield chunk
  173. except Exception as e:
  174. raise MissingAuthError('Missing or invalid "__Secure-1PSID" cookie', e)
  175. if not cls._snlm0e:
  176. if cls._cookies is None or "__Secure-1PSID" not in cls._cookies:
  177. raise MissingAuthError('Missing "__Secure-1PSID" cookie')
  178. await cls.fetch_snlm0e(session, cls._cookies)
  179. if not cls._snlm0e:
  180. raise RuntimeError("Invalid cookies. SNlM0e not found")
  181. if GGOGLE_SID_COOKIE in cls._cookies:
  182. task = cls.rotate_tasks.get(cls._cookies[GGOGLE_SID_COOKIE])
  183. if not task:
  184. cls.rotate_tasks[cls._cookies[GGOGLE_SID_COOKIE]] = asyncio.create_task(
  185. cls.start_auto_refresh()
  186. )
  187. uploads = await cls.upload_images(base_connector, merge_media(media, messages))
  188. async with ClientSession(
  189. cookies=cls._cookies,
  190. headers=REQUEST_HEADERS,
  191. connector=base_connector,
  192. ) as client:
  193. params = {
  194. 'bl': REQUEST_BL_PARAM,
  195. 'hl': language,
  196. '_reqid': random.randint(1111, 9999),
  197. 'rt': 'c',
  198. "f.sid": cls._sid,
  199. }
  200. data = {
  201. 'at': cls._snlm0e,
  202. 'f.req': json.dumps([None, json.dumps(cls.build_request(
  203. prompt,
  204. language=language,
  205. conversation=conversation,
  206. uploads=uploads
  207. ))])
  208. }
  209. async with client.post(
  210. REQUEST_URL,
  211. data=data,
  212. params=params,
  213. headers=models[model] if model in models else None
  214. ) as response:
  215. await raise_for_status(response)
  216. image_prompt = response_part = None
  217. last_content = ""
  218. youtube_ids = []
  219. async for line in response.content:
  220. try:
  221. try:
  222. line = json.loads(line)
  223. except ValueError:
  224. continue
  225. if not isinstance(line, list):
  226. continue
  227. if len(line[0]) < 3 or not line[0][2]:
  228. continue
  229. response_part = json.loads(line[0][2])
  230. if not response_part[4]:
  231. continue
  232. if return_conversation:
  233. yield Conversation(response_part[1][0], response_part[1][1], response_part[4][0][0], model)
  234. def find_youtube_ids(content: str):
  235. pattern = re.compile(r"http://www.youtube.com/watch\?v=([\w-]+)")
  236. for match in pattern.finditer(content):
  237. if match.group(1) not in youtube_ids:
  238. yield match.group(1)
  239. def read_recusive(data):
  240. for item in data:
  241. if isinstance(item, list):
  242. yield from read_recusive(item)
  243. elif isinstance(item, str) and not item.startswith("rc_"):
  244. yield item
  245. def find_str(data, skip=0):
  246. for item in read_recusive(data):
  247. if skip > 0:
  248. skip -= 1
  249. continue
  250. yield item
  251. reasoning = "\n\n".join(find_str(response_part[4][0], 3))
  252. reasoning = re.sub(r"<b>|</b>", "**", reasoning)
  253. def replace_image(match):
  254. return f"![](https:{match.group(0)})"
  255. reasoning = re.sub(r"//yt3.(?:ggpht.com|googleusercontent.com/ytc)/[\w=-]+", replace_image, reasoning)
  256. reasoning = re.sub(r"\nyoutube\n", "\n\n\n", reasoning)
  257. reasoning = re.sub(r"\nyoutube_tool\n", "\n\n", reasoning)
  258. reasoning = re.sub(r"\nYouTube\n", "\nYouTube ", reasoning)
  259. reasoning = reasoning.replace('\nhttps://www.gstatic.com/images/branding/productlogos/youtube/v9/192px.svg', '<i class="fa-brands fa-youtube"></i>')
  260. youtube_ids = list(find_youtube_ids(reasoning))
  261. content = response_part[4][0][1][0]
  262. if reasoning:
  263. yield Reasoning(reasoning, status="🤔")
  264. except (ValueError, KeyError, TypeError, IndexError) as e:
  265. debug.error(f"{cls.__name__} {type(e).__name__}: {e}")
  266. continue
  267. match = re.search(r'\[Imagen of (.*?)\]', content)
  268. if match:
  269. image_prompt = match.group(1)
  270. content = content.replace(match.group(0), '')
  271. pattern = r"http://googleusercontent.com/(?:image_generation|youtube|map)_content/\d+"
  272. content = re.sub(pattern, "", content)
  273. content = content.replace("<!-- end list -->", "")
  274. content = content.replace("<ctrl94>thought", "<think>").replace("<ctrl95>", "</think>")
  275. def replace_link(match):
  276. return f"(https://{quote_plus(unquote_plus(match.group(1)), '/?&=#')})"
  277. content = re.sub(r"\(https://www.google.com/(?:search\?q=|url\?sa=E&source=gmail&q=)https?://(.+?)\)", replace_link, content)
  278. if last_content and content.startswith(last_content):
  279. yield content[len(last_content):]
  280. else:
  281. yield content
  282. last_content = content
  283. if image_prompt:
  284. try:
  285. images = [image[0][3][3] for image in response_part[4][0][12][7][0]]
  286. image_prompt = image_prompt.replace("a fake image", "")
  287. yield ImageResponse(images, image_prompt, {"cookies": cls._cookies})
  288. except (TypeError, IndexError, KeyError):
  289. pass
  290. youtube_ids = youtube_ids if youtube_ids else find_youtube_ids(content)
  291. if youtube_ids:
  292. yield YouTube(youtube_ids)
  293. @classmethod
  294. async def synthesize(cls, params: dict, proxy: str = None) -> AsyncIterator[bytes]:
  295. if "text" not in params:
  296. raise ValueError("Missing parameter text")
  297. async with ClientSession(
  298. cookies=cls._cookies,
  299. headers=REQUEST_HEADERS,
  300. connector=get_connector(proxy=proxy),
  301. ) as session:
  302. if not cls._snlm0e:
  303. await cls.fetch_snlm0e(session, cls._cookies) if cls._cookies else None
  304. inner_data = json.dumps([None, params["text"], "en-US", None, 2])
  305. async with session.post(
  306. "https://gemini.google.com/_/BardChatUi/data/batchexecute",
  307. data={
  308. "f.req": json.dumps([[["XqA3Ic", inner_data, None, "generic"]]]),
  309. "at": cls._snlm0e,
  310. },
  311. params={
  312. "rpcids": "XqA3Ic",
  313. "source-path": "/app/2704fb4aafcca926",
  314. "bl": "boq_assistant-bard-web-server_20241119.00_p1",
  315. "f.sid": "" if cls._sid is None else cls._sid,
  316. "hl": "de",
  317. "_reqid": random.randint(1111, 9999),
  318. "rt": "c"
  319. },
  320. ) as response:
  321. await raise_for_status(response)
  322. iter_base64_response = iter_filter_base64(response.content.iter_chunked(1024))
  323. async for chunk in iter_base64_decode(iter_base64_response):
  324. yield chunk
  325. def build_request(
  326. prompt: str,
  327. language: str,
  328. conversation: Conversation = None,
  329. uploads: list[list[str, str]] = None,
  330. tools: list[list[str]] = []
  331. ) -> list:
  332. image_list = [[[image_url, 1], image_name] for image_url, image_name in uploads] if uploads else []
  333. return [
  334. [prompt, 0, None, image_list, None, None, 0],
  335. [language],
  336. [
  337. None if conversation is None else conversation.conversation_id,
  338. None if conversation is None else conversation.response_id,
  339. None if conversation is None else conversation.choice_id,
  340. None,
  341. None,
  342. []
  343. ],
  344. None,
  345. None,
  346. None,
  347. [1],
  348. 0,
  349. [],
  350. tools,
  351. 1,
  352. 0,
  353. ]
  354. async def upload_images(connector: BaseConnector, media: MediaListType) -> list:
  355. async def upload_image(image: bytes, image_name: str = None):
  356. async with ClientSession(
  357. headers=UPLOAD_IMAGE_HEADERS,
  358. connector=connector
  359. ) as session:
  360. image = to_bytes(image)
  361. async with session.options(UPLOAD_IMAGE_URL) as response:
  362. await raise_for_status(response)
  363. headers = {
  364. "size": str(len(image)),
  365. "x-goog-upload-command": "start"
  366. }
  367. data = f"File name: {image_name}" if image_name else None
  368. async with session.post(
  369. UPLOAD_IMAGE_URL, headers=headers, data=data
  370. ) as response:
  371. await raise_for_status(response)
  372. upload_url = response.headers["X-Goog-Upload-Url"]
  373. async with session.options(upload_url, headers=headers) as response:
  374. await raise_for_status(response)
  375. headers["x-goog-upload-command"] = "upload, finalize"
  376. headers["X-Goog-Upload-Offset"] = "0"
  377. async with session.post(
  378. upload_url, headers=headers, data=image
  379. ) as response:
  380. await raise_for_status(response)
  381. return [await response.text(), image_name]
  382. return await asyncio.gather(*[upload_image(image, image_name) for image, image_name in media])
  383. @classmethod
  384. async def fetch_snlm0e(cls, session: ClientSession, cookies: Cookies):
  385. async with session.get(cls.url, cookies=cookies) as response:
  386. await raise_for_status(response)
  387. response_text = await response.text()
  388. match = re.search(r'SNlM0e\":\"(.*?)\"', response_text)
  389. if match:
  390. cls._snlm0e = match.group(1)
  391. sid_match = re.search(r'"FdrFJe":"([\d-]+)"', response_text)
  392. if sid_match:
  393. cls._sid = sid_match.group(1)
  394. class Conversation(JsonConversation):
  395. def __init__(self,
  396. conversation_id: str,
  397. response_id: str,
  398. choice_id: str,
  399. model: str
  400. ) -> None:
  401. self.conversation_id = conversation_id
  402. self.response_id = response_id
  403. self.choice_id = choice_id
  404. self.model = model
  405. async def iter_filter_base64(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
  406. search_for = b'[["wrb.fr","XqA3Ic","[\\"'
  407. end_with = b'\\'
  408. is_started = False
  409. async for chunk in chunks:
  410. if is_started:
  411. if end_with in chunk:
  412. yield chunk.split(end_with, maxsplit=1).pop(0)
  413. break
  414. else:
  415. yield chunk
  416. elif search_for in chunk:
  417. is_started = True
  418. yield chunk.split(search_for, maxsplit=1).pop()
  419. else:
  420. raise ValueError(f"Response: {chunk}")
  421. async def iter_base64_decode(chunks: AsyncIterator[bytes]) -> AsyncIterator[bytes]:
  422. buffer = b""
  423. rest = 0
  424. async for chunk in chunks:
  425. chunk = buffer + chunk
  426. rest = len(chunk) % 4
  427. buffer = chunk[-rest:]
  428. yield base64.b64decode(chunk[:-rest])
  429. if rest > 0:
  430. yield base64.b64decode(buffer+rest*b"=")
  431. async def rotate_1psidts(url, cookies: dict, proxy: str | None = None) -> str:
  432. path = Path(get_cookies_dir())
  433. path.mkdir(parents=True, exist_ok=True)
  434. filename = f"auth_Gemini.json"
  435. path = path / filename
  436. # Check if the cache file was modified in the last minute to avoid 429 Too Many Requests
  437. if not (path.is_file() and time.time() - os.path.getmtime(path) <= 60):
  438. async with ClientSession(proxy=proxy) as client:
  439. response = await client.post(
  440. url=ROTATE_COOKIES_URL,
  441. headers={
  442. "Content-Type": "application/json",
  443. },
  444. cookies=cookies,
  445. data='[000,"-0000000000000000000"]',
  446. )
  447. if response.status == 401:
  448. raise MissingAuthError("Invalid cookies")
  449. response.raise_for_status()
  450. for key, c in response.cookies.items():
  451. cookies[key] = c.value
  452. new_1psidts = response.cookies.get("__Secure-1PSIDTS")
  453. path.write_text(json.dumps([{
  454. "name": k,
  455. "value": v,
  456. "domain": GOOGLE_COOKIE_DOMAIN,
  457. } for k, v in cookies.items()]))
  458. if new_1psidts:
  459. return new_1psidts